@intlayer/cli 7.5.11 → 7.5.13
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/dist/cjs/cli.cjs +4 -4
- package/dist/cjs/cli.cjs.map +1 -1
- package/dist/cjs/index.cjs +4 -5
- package/dist/cjs/{reviewDoc.cjs → reviewDoc/reviewDoc.cjs} +17 -15
- package/dist/cjs/reviewDoc/reviewDoc.cjs.map +1 -0
- package/dist/cjs/{reviewDocBlockAware.cjs → reviewDoc/reviewDocBlockAware.cjs} +12 -8
- package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs.map +1 -0
- package/dist/cjs/translateDoc/index.cjs +8 -0
- package/dist/cjs/translateDoc/translateDoc.cjs +74 -0
- package/dist/cjs/translateDoc/translateDoc.cjs.map +1 -0
- package/dist/cjs/translateDoc/translateFile.cjs +103 -0
- package/dist/cjs/translateDoc/translateFile.cjs.map +1 -0
- package/dist/cjs/translateDoc/types.cjs +0 -0
- package/dist/cjs/translateDoc/validation.cjs +49 -0
- package/dist/cjs/translateDoc/validation.cjs.map +1 -0
- package/dist/cjs/translation-alignment/planActions.cjs +2 -4
- package/dist/cjs/translation-alignment/planActions.cjs.map +1 -1
- package/dist/cjs/translation-alignment/segmentDocument.cjs +35 -101
- package/dist/cjs/translation-alignment/segmentDocument.cjs.map +1 -1
- package/dist/esm/cli.mjs +2 -2
- package/dist/esm/cli.mjs.map +1 -1
- package/dist/esm/index.mjs +3 -3
- package/dist/esm/{reviewDoc.mjs → reviewDoc/reviewDoc.mjs} +14 -12
- package/dist/esm/reviewDoc/reviewDoc.mjs.map +1 -0
- package/dist/esm/{reviewDocBlockAware.mjs → reviewDoc/reviewDocBlockAware.mjs} +11 -7
- package/dist/esm/reviewDoc/reviewDocBlockAware.mjs.map +1 -0
- package/dist/esm/translateDoc/index.mjs +5 -0
- package/dist/esm/translateDoc/translateDoc.mjs +72 -0
- package/dist/esm/translateDoc/translateDoc.mjs.map +1 -0
- package/dist/esm/translateDoc/translateFile.mjs +102 -0
- package/dist/esm/translateDoc/translateFile.mjs.map +1 -0
- package/dist/esm/translateDoc/types.mjs +0 -0
- package/dist/esm/translateDoc/validation.mjs +47 -0
- package/dist/esm/translateDoc/validation.mjs.map +1 -0
- package/dist/esm/translation-alignment/planActions.mjs +2 -4
- package/dist/esm/translation-alignment/planActions.mjs.map +1 -1
- package/dist/esm/translation-alignment/segmentDocument.mjs +35 -101
- package/dist/esm/translation-alignment/segmentDocument.mjs.map +1 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/{reviewDoc.d.ts → reviewDoc/reviewDoc.d.ts} +1 -1
- package/dist/types/reviewDoc/reviewDoc.d.ts.map +1 -0
- package/dist/types/{reviewDocBlockAware.d.ts → reviewDoc/reviewDocBlockAware.d.ts} +2 -2
- package/dist/types/reviewDoc/reviewDocBlockAware.d.ts.map +1 -0
- package/dist/types/translateDoc/index.d.ts +5 -0
- package/dist/types/translateDoc/translateDoc.d.ts +21 -0
- package/dist/types/translateDoc/translateDoc.d.ts.map +1 -0
- package/dist/types/translateDoc/translateFile.d.ts +21 -0
- package/dist/types/translateDoc/translateFile.d.ts.map +1 -0
- package/dist/types/translateDoc/types.d.ts +47 -0
- package/dist/types/translateDoc/types.d.ts.map +1 -0
- package/dist/types/translateDoc/validation.d.ts +16 -0
- package/dist/types/translateDoc/validation.d.ts.map +1 -0
- package/dist/types/translation-alignment/planActions.d.ts +2 -2
- package/dist/types/translation-alignment/planActions.d.ts.map +1 -1
- package/dist/types/translation-alignment/rebuildDocument.d.ts.map +1 -1
- package/dist/types/translation-alignment/segmentDocument.d.ts.map +1 -1
- package/package.json +11 -11
- package/dist/cjs/reviewDoc.cjs.map +0 -1
- package/dist/cjs/reviewDocBlockAware.cjs.map +0 -1
- package/dist/cjs/translateDoc.cjs +0 -163
- package/dist/cjs/translateDoc.cjs.map +0 -1
- package/dist/esm/reviewDoc.mjs.map +0 -1
- package/dist/esm/reviewDocBlockAware.mjs.map +0 -1
- package/dist/esm/translateDoc.mjs +0 -160
- package/dist/esm/translateDoc.mjs.map +0 -1
- package/dist/types/reviewDoc.d.ts.map +0 -1
- package/dist/types/reviewDocBlockAware.d.ts.map +0 -1
- package/dist/types/translateDoc.d.ts +0 -56
- package/dist/types/translateDoc.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewDoc.mjs","names":["docList: string[]","changedLines: number[] | undefined"],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type { AIOptions } from '@intlayer/api';\nimport {\n formatLocale,\n formatPath,\n type ListGitFilesOptions,\n listGitFiles,\n listGitLines,\n parallelize,\n} from '@intlayer/chokidar';\nimport {\n ANSIColors,\n colorize,\n colorizeNumber,\n type GetConfigurationOptions,\n getAppLogger,\n getConfiguration,\n} from '@intlayer/config';\nimport type { Locale } from '@intlayer/types';\nimport fg from 'fast-glob';\nimport { checkFileModifiedRange } from '../utils/checkFileModifiedRange';\nimport { getOutputFilePath } from '../utils/getOutputFilePath';\nimport { setupAI } from '../utils/setupAI';\nimport { reviewFileBlockAware } from './reviewDocBlockAware';\n\ntype ReviewDocOptions = {\n docPattern: string[];\n locales: Locale[];\n excludedGlobPattern: string[];\n baseLocale: Locale;\n aiOptions?: AIOptions;\n nbSimultaneousFileProcessed?: number;\n configOptions?: GetConfigurationOptions;\n customInstructions?: string;\n skipIfModifiedBefore?: number | string | Date;\n skipIfModifiedAfter?: number | string | Date;\n skipIfExists?: boolean;\n gitOptions?: ListGitFilesOptions;\n};\n\n/**\n * Main audit function: scans all .md files in \"en/\" (unless you specified DOC_LIST),\n * then audits them to each locale in LOCALE_LIST.\n */\nexport const reviewDoc = async ({\n docPattern,\n locales,\n excludedGlobPattern,\n baseLocale,\n aiOptions,\n nbSimultaneousFileProcessed,\n configOptions,\n customInstructions,\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n skipIfExists,\n gitOptions,\n}: ReviewDocOptions) => {\n const configuration = getConfiguration(configOptions);\n const appLogger = getAppLogger(configuration);\n\n const aiResult = await setupAI(configuration, aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig } = aiResult;\n\n if (nbSimultaneousFileProcessed && nbSimultaneousFileProcessed > 10) {\n appLogger(\n `Warning: nbSimultaneousFileProcessed is set to ${nbSimultaneousFileProcessed}, which is greater than 10. Setting it to 10.`\n );\n nbSimultaneousFileProcessed = 10; // Limit the number of simultaneous file processed to 10\n }\n\n let docList: string[] = await fg(docPattern, {\n ignore: excludedGlobPattern,\n });\n\n if (gitOptions) {\n const gitChangedFiles = await listGitFiles(gitOptions);\n\n if (gitChangedFiles) {\n // Convert dictionary file paths to be relative to git root for comparison\n\n // Filter dictionaries based on git changed files\n docList = docList.filter((path) =>\n gitChangedFiles.some((gitFile) => join(process.cwd(), path) === gitFile)\n );\n }\n }\n\n // OAuth handled by API proxy internally\n\n appLogger(`Base locale is ${formatLocale(baseLocale)}`);\n appLogger(\n `Reviewing ${colorizeNumber(locales.length)} locales: [ ${formatLocale(locales)} ]`\n );\n\n appLogger(`Reviewing ${colorizeNumber(docList.length)} files:`);\n appLogger(docList.map((path) => ` - ${formatPath(path)}\\n`));\n\n // Create all tasks to be processed\n const allTasks = docList.flatMap((docPath) =>\n locales.map((locale) => async () => {\n appLogger(\n `Reviewing file: ${formatPath(docPath)} to ${formatLocale(locale)}`\n );\n\n const absoluteBaseFilePath = join(configuration.content.baseDir, docPath);\n const outputFilePath = getOutputFilePath(\n absoluteBaseFilePath,\n locale,\n baseLocale\n );\n\n // Skip if file exists and skipIfExists option is enabled\n if (skipIfExists && existsSync(outputFilePath)) {\n const relativePath = relative(\n configuration.content.baseDir,\n outputFilePath\n );\n appLogger(\n `${colorize('⊘', ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`\n );\n return;\n }\n\n // Check modification range only if the file exists\n if (existsSync(outputFilePath)) {\n const fileModificationData = checkFileModifiedRange(outputFilePath, {\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n });\n\n if (fileModificationData.isSkipped) {\n appLogger(fileModificationData.message);\n return;\n }\n } else if (skipIfModifiedBefore || skipIfModifiedAfter) {\n // Log if we intended to check modification time but couldn't because the file doesn't exist\n appLogger(\n `${colorize('!', ANSIColors.YELLOW)} File ${formatPath(outputFilePath)} does not exist, skipping modification date check.`\n );\n }\n\n let changedLines: number[] | undefined;\n // FIXED: Enable git optimization that was previously commented out\n if (gitOptions) {\n const gitChangedLines = await listGitLines(\n absoluteBaseFilePath,\n gitOptions\n );\n\n appLogger(`Git changed lines: ${gitChangedLines.join(', ')}`);\n changedLines = gitChangedLines;\n }\n\n await reviewFileBlockAware(\n absoluteBaseFilePath,\n outputFilePath,\n locale as Locale,\n baseLocale,\n aiOptions,\n configOptions,\n customInstructions,\n changedLines,\n aiClient,\n aiConfig\n );\n })\n );\n\n await parallelize(\n allTasks,\n (task) => task(),\n nbSimultaneousFileProcessed ?? 3\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AA6CA,MAAa,YAAY,OAAO,EAC9B,YACA,SACA,qBACA,YACA,WACA,6BACA,eACA,oBACA,sBACA,qBACA,cACA,iBACsB;CACtB,MAAM,gBAAgB,iBAAiB,cAAc;CACrD,MAAM,YAAY,aAAa,cAAc;CAE7C,MAAM,WAAW,MAAM,QAAQ,eAAe,UAAU;AAExD,KAAI,CAAC,UAAU,YAAa;CAE5B,MAAM,EAAE,UAAU,aAAa;AAE/B,KAAI,+BAA+B,8BAA8B,IAAI;AACnE,YACE,kDAAkD,4BAA4B,+CAC/E;AACD,gCAA8B;;CAGhC,IAAIA,UAAoB,MAAM,GAAG,YAAY,EAC3C,QAAQ,qBACT,CAAC;AAEF,KAAI,YAAY;EACd,MAAM,kBAAkB,MAAM,aAAa,WAAW;AAEtD,MAAI,gBAIF,WAAU,QAAQ,QAAQ,SACxB,gBAAgB,MAAM,YAAY,KAAK,QAAQ,KAAK,EAAE,KAAK,KAAK,QAAQ,CACzE;;AAML,WAAU,kBAAkB,aAAa,WAAW,GAAG;AACvD,WACE,aAAa,eAAe,QAAQ,OAAO,CAAC,cAAc,aAAa,QAAQ,CAAC,IACjF;AAED,WAAU,aAAa,eAAe,QAAQ,OAAO,CAAC,SAAS;AAC/D,WAAU,QAAQ,KAAK,SAAS,MAAM,WAAW,KAAK,CAAC,IAAI,CAAC;AAyE5D,OAAM,YAtEW,QAAQ,SAAS,YAChC,QAAQ,KAAK,WAAW,YAAY;AAClC,YACE,mBAAmB,WAAW,QAAQ,CAAC,MAAM,aAAa,OAAO,GAClE;EAED,MAAM,uBAAuB,KAAK,cAAc,QAAQ,SAAS,QAAQ;EACzE,MAAM,iBAAiB,kBACrB,sBACA,QACA,WACD;AAGD,MAAI,gBAAgB,WAAW,eAAe,EAAE;GAC9C,MAAM,eAAe,SACnB,cAAc,QAAQ,SACtB,eACD;AACD,aACE,GAAG,SAAS,KAAK,WAAW,OAAO,CAAC,QAAQ,WAAW,aAAa,CAAC,4BACtE;AACD;;AAIF,MAAI,WAAW,eAAe,EAAE;GAC9B,MAAM,uBAAuB,uBAAuB,gBAAgB;IAClE;IACA;IACD,CAAC;AAEF,OAAI,qBAAqB,WAAW;AAClC,cAAU,qBAAqB,QAAQ;AACvC;;aAEO,wBAAwB,oBAEjC,WACE,GAAG,SAAS,KAAK,WAAW,OAAO,CAAC,QAAQ,WAAW,eAAe,CAAC,oDACxE;EAGH,IAAIC;AAEJ,MAAI,YAAY;GACd,MAAM,kBAAkB,MAAM,aAC5B,sBACA,WACD;AAED,aAAU,sBAAsB,gBAAgB,KAAK,KAAK,GAAG;AAC7D,kBAAe;;AAGjB,QAAM,qBACJ,sBACA,gBACA,QACA,YACA,WACA,eACA,oBACA,cACA,UACA,SACD;GACD,CACH,GAIE,SAAS,MAAM,EAChB,+BAA+B,EAChC"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readAsset } from "
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { readAsset } from "../_virtual/_utils_asset.mjs";
|
|
2
|
+
import { sanitizeChunk, validateTranslation } from "../translateDoc/validation.mjs";
|
|
3
|
+
import { mergeReviewedSegments } from "../translation-alignment/rebuildDocument.mjs";
|
|
4
|
+
import { buildAlignmentPlan } from "../translation-alignment/pipeline.mjs";
|
|
5
|
+
import { chunkInference } from "../utils/chunkInference.mjs";
|
|
6
|
+
import { fixChunkStartEndChars } from "../utils/fixChunkStartEndChars.mjs";
|
|
6
7
|
import { formatLocale, formatPath } from "@intlayer/chokidar";
|
|
7
8
|
import { ANSIColors, colon, colorize, colorizeNumber, getAppLogger, getConfiguration, retryManager } from "@intlayer/config";
|
|
8
9
|
import { dirname } from "node:path";
|
|
@@ -11,7 +12,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
11
12
|
import { readFile } from "node:fs/promises";
|
|
12
13
|
import { Locales } from "@intlayer/types";
|
|
13
14
|
|
|
14
|
-
//#region src/reviewDocBlockAware.ts
|
|
15
|
+
//#region src/reviewDoc/reviewDocBlockAware.ts
|
|
15
16
|
/**
|
|
16
17
|
* Review a file using block-aware alignment.
|
|
17
18
|
* This approach:
|
|
@@ -74,7 +75,10 @@ const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLo
|
|
|
74
75
|
}
|
|
75
76
|
], aiOptions, configuration, aiClient, aiConfig);
|
|
76
77
|
applicationLogger(`${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}`);
|
|
77
|
-
|
|
78
|
+
let processedChunk = sanitizeChunk(result?.fileContent, englishBlock.content);
|
|
79
|
+
processedChunk = fixChunkStartEndChars(processedChunk, englishBlock.content);
|
|
80
|
+
if (!validateTranslation(englishBlock.content, processedChunk, applicationLogger)) throw new Error("Validation failed for chunk (structure or length mismatch). Retrying...");
|
|
81
|
+
return processedChunk;
|
|
78
82
|
})();
|
|
79
83
|
reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult);
|
|
80
84
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewDocBlockAware.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocBlockAware.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { readAsset } from 'utils:asset';\nimport type { AIConfig } from '@intlayer/ai';\nimport type { AIOptions } from '@intlayer/api';\nimport { formatLocale, formatPath } from '@intlayer/chokidar';\nimport {\n ANSIColors,\n colon,\n colorize,\n colorizeNumber,\n type GetConfigurationOptions,\n getAppLogger,\n getConfiguration,\n retryManager,\n} from '@intlayer/config';\nimport { getLocaleName } from '@intlayer/core';\nimport { type Locale, Locales } from '@intlayer/types';\nimport { sanitizeChunk, validateTranslation } from '../translateDoc/validation';\nimport {\n buildAlignmentPlan,\n mergeReviewedSegments,\n} from '../translation-alignment/pipeline';\nimport { chunkInference } from '../utils/chunkInference';\nimport { fixChunkStartEndChars } from '../utils/fixChunkStartEndChars';\nimport type { AIClient } from '../utils/setupAI';\n\n/**\n * Review a file using block-aware alignment.\n * This approach:\n * 1. Segments both English and French documents into semantic blocks\n * 2. Aligns blocks using structure (special chars, numbers) and context\n * 3. Detects which blocks changed, were added, or deleted\n * 4. Only sends changed/new blocks to AI for translation\n * 5. Handles reordering automatically\n */\nexport const reviewFileBlockAware = async (\n baseFilePath: string,\n outputFilePath: string,\n locale: Locale,\n baseLocale: Locale,\n aiOptions?: AIOptions,\n configOptions?: GetConfigurationOptions,\n customInstructions?: string,\n changedLines?: number[],\n aiClient?: AIClient,\n aiConfig?: AIConfig\n) => {\n const configuration = getConfiguration(configOptions);\n const applicationLogger = getAppLogger(configuration);\n\n const englishText = await readFile(baseFilePath, 'utf-8');\n const frenchText = await readFile(outputFilePath, 'utf-8').catch(() => '');\n\n const basePrompt = readAsset('./prompts/REVIEW_PROMPT.md', 'utf-8')\n .replaceAll('{{localeName}}', `${formatLocale(locale, false)}`)\n .replaceAll('{{baseLocaleName}}', `${formatLocale(baseLocale, false)}`)\n .replace('{{applicationContext}}', aiOptions?.applicationContext ?? '-')\n .replace('{{customInstructions}}', customInstructions ?? '-');\n\n const filePrefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `;\n const filePrefix = [\n colon(filePrefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n const prefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `;\n const prefix = [\n colon(prefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n\n // Build block-aware alignment and plan\n const { englishBlocks, frenchBlocks, plan, segmentsToReview } =\n buildAlignmentPlan({\n englishText,\n frenchText,\n changedLines,\n });\n\n applicationLogger(\n `${filePrefix}Block-aware alignment complete. Total blocks: EN=${colorizeNumber(englishBlocks.length)}, FR=${colorizeNumber(frenchBlocks.length)}`\n );\n applicationLogger(\n `${filePrefix}Actions: reuse=${colorizeNumber(plan.actions.filter((a) => a.kind === 'reuse').length)}, review=${colorizeNumber(plan.actions.filter((a) => a.kind === 'review').length)}, new=${colorizeNumber(plan.actions.filter((a) => a.kind === 'insert_new').length)}, delete=${colorizeNumber(plan.actions.filter((a) => a.kind === 'delete').length)}`\n );\n\n if (segmentsToReview.length === 0) {\n applicationLogger(\n `${filePrefix}No segments need review, reusing existing translation`\n );\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(\n outputFilePath,\n mergeReviewedSegments(plan, frenchBlocks, new Map())\n );\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no changes needed).`\n );\n return;\n }\n\n applicationLogger(\n `${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)}`\n );\n\n // Review segments that need AI translation\n const reviewedSegmentsMap = new Map<number, string>();\n\n for (const segment of segmentsToReview) {\n const segmentNumber = segmentsToReview.indexOf(segment) + 1;\n const englishBlock = segment.englishBlock;\n\n const getBaseChunkContextPrompt = () =>\n `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\\n` +\n `///chunksStart///\\n` +\n englishBlock.content +\n `///chunksEnd///`;\n\n const getFrenchChunkPrompt = () =>\n `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\\n` +\n `///chunksStart///\\n` +\n (segment.frenchBlockText ?? '') +\n `///chunksEnd///`;\n\n const reviewedChunkResult = await retryManager(async () => {\n const result = await chunkInference(\n [\n { role: 'system', content: basePrompt },\n { role: 'system', content: getBaseChunkContextPrompt() },\n { role: 'system', content: getFrenchChunkPrompt() },\n {\n role: 'system',\n content: `The next user message will be the **BLOCK ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}** that should be translated in ${getLocaleName(locale, Locales.ENGLISH)} (${locale}).`,\n },\n { role: 'user', content: englishBlock.content },\n ],\n aiOptions,\n configuration,\n aiClient,\n aiConfig\n );\n\n applicationLogger(\n `${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}`\n );\n\n // Sanitize artifacts (e.g. Markdown code block wrappers)\n let processedChunk = sanitizeChunk(\n result?.fileContent,\n englishBlock.content\n );\n\n // Fix start/end characters\n processedChunk = fixChunkStartEndChars(\n processedChunk,\n englishBlock.content\n );\n\n // Validate Translation (YAML, Code fences, Length ratio)\n const isValid = validateTranslation(\n englishBlock.content,\n processedChunk,\n applicationLogger\n );\n\n if (!isValid) {\n throw new Error(\n 'Validation failed for chunk (structure or length mismatch). Retrying...'\n );\n }\n\n return processedChunk;\n })();\n\n reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult);\n }\n\n // Merge reviewed segments back into final document\n const finalFrenchOutput = mergeReviewedSegments(\n plan,\n frenchBlocks,\n reviewedSegmentsMap\n );\n\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, finalFrenchOutput);\n\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.`\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { setupAI } from "../utils/setupAI.mjs";
|
|
2
|
+
import { checkFileModifiedRange } from "../utils/checkFileModifiedRange.mjs";
|
|
3
|
+
import { getOutputFilePath } from "../utils/getOutputFilePath.mjs";
|
|
4
|
+
import { translateFile } from "./translateFile.mjs";
|
|
5
|
+
import { listGitFiles, pLimit, parallelize } from "@intlayer/chokidar";
|
|
6
|
+
import { ANSIColors, colorize, colorizeNumber, getAppLogger, getConfiguration } from "@intlayer/config";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import fg from "fast-glob";
|
|
10
|
+
import { performance } from "node:perf_hooks";
|
|
11
|
+
|
|
12
|
+
//#region src/translateDoc/translateDoc.ts
|
|
13
|
+
const translateDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed = 20, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, flushStrategy = "incremental" }) => {
|
|
14
|
+
const configuration = getConfiguration(configOptions);
|
|
15
|
+
const appLogger = getAppLogger(configuration);
|
|
16
|
+
const maxConcurrentChunks = nbSimultaneousFileProcessed;
|
|
17
|
+
const globalChunkLimiter = pLimit(maxConcurrentChunks);
|
|
18
|
+
let docList = await fg(docPattern, { ignore: excludedGlobPattern });
|
|
19
|
+
const aiResult = await setupAI(configuration, aiOptions);
|
|
20
|
+
if (!aiResult?.hasAIAccess) return;
|
|
21
|
+
const { aiClient, aiConfig } = aiResult;
|
|
22
|
+
if (gitOptions) {
|
|
23
|
+
const gitChangedFiles = await listGitFiles(gitOptions);
|
|
24
|
+
if (gitChangedFiles) docList = docList.filter((path) => gitChangedFiles.some((gitFile) => join(process.cwd(), path) === gitFile));
|
|
25
|
+
}
|
|
26
|
+
const batchStartTime = performance.now();
|
|
27
|
+
appLogger(`Translating ${colorizeNumber(docList.length)} files to ${colorizeNumber(locales.length)} locales. \nGlobal Concurrency: ${colorizeNumber(maxConcurrentChunks)} chunks in parallel.`);
|
|
28
|
+
const errorState = {
|
|
29
|
+
count: 0,
|
|
30
|
+
maxErrors: 5,
|
|
31
|
+
shouldStop: false
|
|
32
|
+
};
|
|
33
|
+
await parallelize(docList.flatMap((docPath) => locales.map((locale) => async () => {
|
|
34
|
+
if (errorState.shouldStop) return;
|
|
35
|
+
const absoluteBaseFilePath = join(configuration.content.baseDir, docPath);
|
|
36
|
+
const outputFilePath = getOutputFilePath(absoluteBaseFilePath, locale, baseLocale);
|
|
37
|
+
if (skipIfExists && existsSync(outputFilePath)) return;
|
|
38
|
+
if (flushStrategy === "incremental" && !existsSync(outputFilePath)) {
|
|
39
|
+
mkdirSync(dirname(outputFilePath), { recursive: true });
|
|
40
|
+
writeFileSync(outputFilePath, "");
|
|
41
|
+
}
|
|
42
|
+
const fileModificationData = checkFileModifiedRange(outputFilePath, {
|
|
43
|
+
skipIfModifiedBefore,
|
|
44
|
+
skipIfModifiedAfter
|
|
45
|
+
});
|
|
46
|
+
if (fileModificationData.isSkipped) {
|
|
47
|
+
appLogger(fileModificationData.message);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await translateFile({
|
|
51
|
+
baseFilePath: absoluteBaseFilePath,
|
|
52
|
+
outputFilePath,
|
|
53
|
+
locale,
|
|
54
|
+
baseLocale,
|
|
55
|
+
configuration,
|
|
56
|
+
errorState,
|
|
57
|
+
aiOptions,
|
|
58
|
+
customInstructions,
|
|
59
|
+
aiClient,
|
|
60
|
+
aiConfig,
|
|
61
|
+
flushStrategy,
|
|
62
|
+
limit: globalChunkLimiter
|
|
63
|
+
});
|
|
64
|
+
})), (task) => task(), 50);
|
|
65
|
+
const batchDuration = ((performance.now() - batchStartTime) / 1e3).toFixed(2);
|
|
66
|
+
if (errorState.count > 0) appLogger(`Finished with ${errorState.count} errors in ${batchDuration}s.`);
|
|
67
|
+
else appLogger(`${colorize("✔", ANSIColors.GREEN)} Batch completed successfully in ${colorizeNumber(batchDuration)}s.`);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { translateDoc };
|
|
72
|
+
//# sourceMappingURL=translateDoc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translateDoc.mjs","names":["docList: string[]","errorState: ErrorState"],"sources":["../../../src/translateDoc/translateDoc.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { performance } from 'node:perf_hooks';\nimport { listGitFiles, parallelize, pLimit } from '@intlayer/chokidar';\nimport {\n ANSIColors,\n colorize,\n colorizeNumber,\n getAppLogger,\n getConfiguration,\n} from '@intlayer/config';\nimport type { Locale } from '@intlayer/types';\nimport fg from 'fast-glob';\nimport { checkFileModifiedRange } from '../utils/checkFileModifiedRange';\nimport { getOutputFilePath } from '../utils/getOutputFilePath';\nimport { setupAI } from '../utils/setupAI';\nimport { translateFile } from './translateFile';\nimport type { ErrorState, TranslateDocOptions } from './types';\n\nexport const translateDoc = async ({\n docPattern,\n locales,\n excludedGlobPattern,\n baseLocale,\n aiOptions,\n nbSimultaneousFileProcessed = 20, // Default to a higher concurrency for chunks\n configOptions,\n customInstructions,\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n skipIfExists,\n gitOptions,\n flushStrategy = 'incremental',\n}: TranslateDocOptions) => {\n const configuration = getConfiguration(configOptions);\n const appLogger = getAppLogger(configuration);\n\n // 1. GLOBAL QUEUE SETUP\n // We use pLimit to create a single bottleneck for AI requests.\n // This queue is shared across all files and locales.\n const maxConcurrentChunks = nbSimultaneousFileProcessed;\n const globalChunkLimiter = pLimit(maxConcurrentChunks);\n\n let docList: string[] = await fg(docPattern, {\n ignore: excludedGlobPattern,\n });\n\n const aiResult = await setupAI(configuration, aiOptions);\n if (!aiResult?.hasAIAccess) return;\n const { aiClient, aiConfig } = aiResult;\n\n if (gitOptions) {\n const gitChangedFiles = await listGitFiles(gitOptions);\n if (gitChangedFiles) {\n docList = docList.filter((path) =>\n gitChangedFiles.some((gitFile) => join(process.cwd(), path) === gitFile)\n );\n }\n }\n\n const batchStartTime = performance.now();\n\n appLogger(\n `Translating ${colorizeNumber(docList.length)} files to ${colorizeNumber(locales.length)} locales. \\n` +\n `Global Concurrency: ${colorizeNumber(maxConcurrentChunks)} chunks in parallel.`\n );\n\n const errorState: ErrorState = {\n count: 0,\n maxErrors: 5,\n shouldStop: false,\n };\n\n // 2. FLATTENED TASK LIST\n // We create a task for every File x Locale combination.\n const allTasks = docList.flatMap((docPath) =>\n locales.map((locale) => async () => {\n if (errorState.shouldStop) return;\n\n const absoluteBaseFilePath = join(configuration.content.baseDir, docPath);\n const outputFilePath = getOutputFilePath(\n absoluteBaseFilePath,\n locale,\n baseLocale\n );\n\n // Skip logic\n if (skipIfExists && existsSync(outputFilePath)) return;\n\n if (flushStrategy === 'incremental' && !existsSync(outputFilePath)) {\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, '');\n }\n\n const fileModificationData = checkFileModifiedRange(outputFilePath, {\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n });\n\n if (fileModificationData.isSkipped) {\n appLogger(fileModificationData.message);\n return;\n }\n\n // Execute translation using the SHARED limiter\n await translateFile({\n baseFilePath: absoluteBaseFilePath,\n outputFilePath,\n locale: locale as Locale,\n baseLocale,\n configuration,\n errorState,\n aiOptions,\n customInstructions,\n aiClient,\n aiConfig,\n flushStrategy,\n limit: globalChunkLimiter, // Pass the global queue\n });\n })\n );\n\n // 3. HIGH-THROUGHPUT FILE OPENER\n // We open many files simultaneously (e.g., 50) to ensure the global chunk queue\n // is always saturated with work.\n // If we open too few files, the chunk queue might drain faster than we can read new files.\n const FILE_OPEN_LIMIT = 50;\n\n await parallelize(allTasks, (task) => task(), FILE_OPEN_LIMIT);\n\n const batchEndTime = performance.now();\n const batchDuration = ((batchEndTime - batchStartTime) / 1000).toFixed(2);\n\n if (errorState.count > 0) {\n appLogger(`Finished with ${errorState.count} errors in ${batchDuration}s.`);\n } else {\n appLogger(\n `${colorize('✔', ANSIColors.GREEN)} Batch completed successfully in ${colorizeNumber(batchDuration)}s.`\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAa,eAAe,OAAO,EACjC,YACA,SACA,qBACA,YACA,WACA,8BAA8B,IAC9B,eACA,oBACA,sBACA,qBACA,cACA,YACA,gBAAgB,oBACS;CACzB,MAAM,gBAAgB,iBAAiB,cAAc;CACrD,MAAM,YAAY,aAAa,cAAc;CAK7C,MAAM,sBAAsB;CAC5B,MAAM,qBAAqB,OAAO,oBAAoB;CAEtD,IAAIA,UAAoB,MAAM,GAAG,YAAY,EAC3C,QAAQ,qBACT,CAAC;CAEF,MAAM,WAAW,MAAM,QAAQ,eAAe,UAAU;AACxD,KAAI,CAAC,UAAU,YAAa;CAC5B,MAAM,EAAE,UAAU,aAAa;AAE/B,KAAI,YAAY;EACd,MAAM,kBAAkB,MAAM,aAAa,WAAW;AACtD,MAAI,gBACF,WAAU,QAAQ,QAAQ,SACxB,gBAAgB,MAAM,YAAY,KAAK,QAAQ,KAAK,EAAE,KAAK,KAAK,QAAQ,CACzE;;CAIL,MAAM,iBAAiB,YAAY,KAAK;AAExC,WACE,eAAe,eAAe,QAAQ,OAAO,CAAC,YAAY,eAAe,QAAQ,OAAO,CAAC,kCAChE,eAAe,oBAAoB,CAAC,sBAC9D;CAED,MAAMC,aAAyB;EAC7B,OAAO;EACP,WAAW;EACX,YAAY;EACb;AAyDD,OAAM,YArDW,QAAQ,SAAS,YAChC,QAAQ,KAAK,WAAW,YAAY;AAClC,MAAI,WAAW,WAAY;EAE3B,MAAM,uBAAuB,KAAK,cAAc,QAAQ,SAAS,QAAQ;EACzE,MAAM,iBAAiB,kBACrB,sBACA,QACA,WACD;AAGD,MAAI,gBAAgB,WAAW,eAAe,CAAE;AAEhD,MAAI,kBAAkB,iBAAiB,CAAC,WAAW,eAAe,EAAE;AAClE,aAAU,QAAQ,eAAe,EAAE,EAAE,WAAW,MAAM,CAAC;AACvD,iBAAc,gBAAgB,GAAG;;EAGnC,MAAM,uBAAuB,uBAAuB,gBAAgB;GAClE;GACA;GACD,CAAC;AAEF,MAAI,qBAAqB,WAAW;AAClC,aAAU,qBAAqB,QAAQ;AACvC;;AAIF,QAAM,cAAc;GAClB,cAAc;GACd;GACQ;GACR;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,OAAO;GACR,CAAC;GACF,CACH,GAQ4B,SAAS,MAAM,EAFpB,GAEsC;CAG9D,MAAM,kBADe,YAAY,KAAK,GACC,kBAAkB,KAAM,QAAQ,EAAE;AAEzE,KAAI,WAAW,QAAQ,EACrB,WAAU,iBAAiB,WAAW,MAAM,aAAa,cAAc,IAAI;KAE3E,WACE,GAAG,SAAS,KAAK,WAAW,MAAM,CAAC,mCAAmC,eAAe,cAAc,CAAC,IACrG"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readAsset } from "../_virtual/_utils_asset.mjs";
|
|
2
|
+
import { sanitizeChunk, validateTranslation } from "./validation.mjs";
|
|
3
|
+
import { chunkInference } from "../utils/chunkInference.mjs";
|
|
4
|
+
import { fixChunkStartEndChars } from "../utils/fixChunkStartEndChars.mjs";
|
|
5
|
+
import { chunkText } from "../utils/calculateChunks.mjs";
|
|
6
|
+
import { formatLocale, formatPath } from "@intlayer/chokidar";
|
|
7
|
+
import { ANSIColors, colon, colorize, colorizeNumber, getAppLogger, retryManager } from "@intlayer/config";
|
|
8
|
+
import { dirname, relative } from "node:path";
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { performance } from "node:perf_hooks";
|
|
12
|
+
|
|
13
|
+
//#region src/translateDoc/translateFile.ts
|
|
14
|
+
const translateFile = async ({ baseFilePath, outputFilePath, locale, baseLocale, configuration, errorState, aiOptions, customInstructions, aiClient, aiConfig, flushStrategy = "incremental", onChunkReceive, limit }) => {
|
|
15
|
+
if (errorState.shouldStop) return null;
|
|
16
|
+
const appLogger = getAppLogger(configuration, { config: { prefix: "" } });
|
|
17
|
+
const fileStartTime = performance.now();
|
|
18
|
+
try {
|
|
19
|
+
const chunks = chunkText(await readFile(baseFilePath, "utf-8"));
|
|
20
|
+
const totalChunks = chunks.length;
|
|
21
|
+
const filePrefix = `${colon(`${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `, { colSize: 40 })}${ANSIColors.RESET}`;
|
|
22
|
+
const prefix = `${colon(`${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `, { colSize: 40 })}${ANSIColors.RESET}`;
|
|
23
|
+
appLogger(`${filePrefix}Split into ${colorizeNumber(totalChunks)} chunks. Queuing...`);
|
|
24
|
+
const basePrompt = readAsset("./prompts/TRANSLATE_PROMPT.md", "utf-8").replaceAll("{{localeName}}", `${formatLocale(locale, false)}`).replaceAll("{{baseLocaleName}}", `${formatLocale(baseLocale, false)}`).replace("{{applicationContext}}", aiOptions?.applicationContext ?? "-").replace("{{customInstructions}}", customInstructions ?? "-");
|
|
25
|
+
const translatedParts = new Array(totalChunks).fill("");
|
|
26
|
+
const runTask = limit ?? ((fn) => fn());
|
|
27
|
+
const tasks = chunks.map((chunk, i) => runTask(async () => {
|
|
28
|
+
if (errorState.shouldStop) return null;
|
|
29
|
+
const chunkLogger = getAppLogger(configuration, { config: { prefix: `${prefix} ${ANSIColors.GREY_DARK}[${i + 1}/${totalChunks}] ${ANSIColors.RESET}` } });
|
|
30
|
+
const chunkStartTime = performance.now();
|
|
31
|
+
const isFirstChunk = i === 0;
|
|
32
|
+
const fileToTranslateCurrentChunk = chunk.content;
|
|
33
|
+
const getPrevChunkPrompt = () => `>>> CONTEXT: PREVIOUS SOURCE CONTENT <<<\n\`\`\`\n` + (chunks[i - 1]?.content ?? "") + `\n\`\`\`\n>>> END PREVIOUS CONTEXT <<<`;
|
|
34
|
+
const getBaseChunkContextPrompt = () => `>>> CONTEXT: NEXT CONTENT <<<\n\`\`\`\n` + (chunks[i + 1]?.content ?? "") + `\n\`\`\`\n>>> END NEXT CONTEXT <<<`;
|
|
35
|
+
chunkLogger("Process started");
|
|
36
|
+
const { content: translatedChunk, tokens } = await retryManager(async () => {
|
|
37
|
+
const result = await chunkInference([
|
|
38
|
+
{
|
|
39
|
+
role: "system",
|
|
40
|
+
content: basePrompt
|
|
41
|
+
},
|
|
42
|
+
...chunks[i + 1] ? [{
|
|
43
|
+
role: "system",
|
|
44
|
+
content: getBaseChunkContextPrompt()
|
|
45
|
+
}] : [],
|
|
46
|
+
...isFirstChunk ? [] : [{
|
|
47
|
+
role: "system",
|
|
48
|
+
content: getPrevChunkPrompt()
|
|
49
|
+
}],
|
|
50
|
+
{
|
|
51
|
+
role: "system",
|
|
52
|
+
content: [`You are translating TARGET CHUNK (${i + 1}/${totalChunks}).`, `Translate ONLY the target chunk. Preserve frontmatter/code exactly.`].join("\n")
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
role: "user",
|
|
56
|
+
content: `>>> TARGET CHUNK START <<<\n${fileToTranslateCurrentChunk}\n>>> TARGET CHUNK END <<<`
|
|
57
|
+
}
|
|
58
|
+
], aiOptions, configuration, aiClient, aiConfig);
|
|
59
|
+
let processedChunk = sanitizeChunk(result?.fileContent, fileToTranslateCurrentChunk);
|
|
60
|
+
processedChunk = fixChunkStartEndChars(processedChunk, fileToTranslateCurrentChunk);
|
|
61
|
+
if (!validateTranslation(fileToTranslateCurrentChunk, processedChunk, chunkLogger)) throw new Error(`Validation failed for chunk ${i + 1}/${totalChunks}`);
|
|
62
|
+
return {
|
|
63
|
+
content: processedChunk,
|
|
64
|
+
tokens: result.tokenUsed
|
|
65
|
+
};
|
|
66
|
+
})();
|
|
67
|
+
const chunkDuration = (performance.now() - chunkStartTime).toFixed(0);
|
|
68
|
+
translatedParts[i] = translatedChunk;
|
|
69
|
+
if (onChunkReceive) onChunkReceive(translatedChunk, i, totalChunks);
|
|
70
|
+
if (flushStrategy === "incremental") {
|
|
71
|
+
if (translatedParts.slice(0, i + 1).every((p) => p && p !== "")) {
|
|
72
|
+
let endIdx = 0;
|
|
73
|
+
while (endIdx < totalChunks && translatedParts[endIdx] && translatedParts[endIdx] !== "") endIdx++;
|
|
74
|
+
const currentContent = translatedParts.slice(0, endIdx).join("");
|
|
75
|
+
mkdirSync(dirname(outputFilePath), { recursive: true });
|
|
76
|
+
writeFileSync(outputFilePath, currentContent);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
chunkLogger([`${colorizeNumber(tokens)} tokens used `, `${ANSIColors.GREY_DARK}in ${colorizeNumber(chunkDuration)}ms${ANSIColors.RESET}`].join(""));
|
|
80
|
+
}));
|
|
81
|
+
await Promise.all(tasks);
|
|
82
|
+
const fullContent = translatedParts.join("");
|
|
83
|
+
if (flushStrategy === "end" || flushStrategy === "incremental") {
|
|
84
|
+
mkdirSync(dirname(outputFilePath), { recursive: true });
|
|
85
|
+
writeFileSync(outputFilePath, fullContent);
|
|
86
|
+
}
|
|
87
|
+
const totalDuration = ((performance.now() - fileStartTime) / 1e3).toFixed(2);
|
|
88
|
+
const relativePath = relative(configuration.content.baseDir, outputFilePath);
|
|
89
|
+
appLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(relativePath)} completed in ${colorizeNumber(totalDuration)}s.`);
|
|
90
|
+
return fullContent;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
errorState.count++;
|
|
93
|
+
const errorMessage = error?.message ?? JSON.stringify(error);
|
|
94
|
+
appLogger(`${colorize("✖", ANSIColors.RED)} Error: ${errorMessage}`);
|
|
95
|
+
if (errorState.count >= errorState.maxErrors) errorState.shouldStop = true;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { translateFile };
|
|
102
|
+
//# sourceMappingURL=translateFile.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translateFile.mjs","names":["translatedParts: string[]","error: any"],"sources":["../../../src/translateDoc/translateFile.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, relative } from 'node:path';\nimport { performance } from 'node:perf_hooks';\nimport { readAsset } from 'utils:asset';\nimport { formatLocale, formatPath } from '@intlayer/chokidar';\nimport {\n ANSIColors,\n colon,\n colorize,\n colorizeNumber,\n getAppLogger,\n retryManager,\n} from '@intlayer/config';\nimport { chunkText } from '../utils/calculateChunks';\nimport { chunkInference } from '../utils/chunkInference';\nimport { fixChunkStartEndChars } from '../utils/fixChunkStartEndChars';\nimport type { TranslateFileOptions } from './types';\nimport { sanitizeChunk, validateTranslation } from './validation';\n\nexport const translateFile = async ({\n baseFilePath,\n outputFilePath,\n locale,\n baseLocale,\n configuration,\n errorState,\n aiOptions,\n customInstructions,\n aiClient,\n aiConfig,\n flushStrategy = 'incremental',\n onChunkReceive,\n limit, // The Global Limiter\n}: TranslateFileOptions): Promise<string | null> => {\n if (errorState.shouldStop) return null;\n\n const appLogger = getAppLogger(configuration, { config: { prefix: '' } });\n const fileStartTime = performance.now();\n\n try {\n const fileContent = await readFile(baseFilePath, 'utf-8');\n const chunks = chunkText(fileContent);\n const totalChunks = chunks.length;\n\n const filePrefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `;\n const filePrefix = `${colon(filePrefixText, { colSize: 40 })}${ANSIColors.RESET}`;\n const prefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `;\n const prefix = `${colon(prefixText, { colSize: 40 })}${ANSIColors.RESET}`;\n\n appLogger(\n `${filePrefix}Split into ${colorizeNumber(totalChunks)} chunks. Queuing...`\n );\n\n const basePrompt = readAsset('./prompts/TRANSLATE_PROMPT.md', 'utf-8')\n .replaceAll('{{localeName}}', `${formatLocale(locale, false)}`)\n .replaceAll('{{baseLocaleName}}', `${formatLocale(baseLocale, false)}`)\n .replace('{{applicationContext}}', aiOptions?.applicationContext ?? '-')\n .replace('{{customInstructions}}', customInstructions ?? '-');\n\n const translatedParts: string[] = new Array(totalChunks).fill('');\n\n // Fallback if no limiter is provided (runs immediately)\n const runTask = limit ?? ((fn) => fn());\n\n // MAP CHUNKS TO GLOBAL TASKS\n // This pushes ALL chunks for this file into the Global Queue immediately.\n // They will execute whenever the global concurrency slots open up.\n const tasks = chunks.map((chunk, i) =>\n runTask(async () => {\n if (errorState.shouldStop) return null;\n\n const chunkLogger = getAppLogger(configuration, {\n config: {\n prefix: `${prefix} ${ANSIColors.GREY_DARK}[${i + 1}/${totalChunks}] ${ANSIColors.RESET}`,\n },\n });\n\n const chunkStartTime = performance.now();\n const isFirstChunk = i === 0;\n const fileToTranslateCurrentChunk = chunk.content;\n\n // Context Preparation\n const getPrevChunkPrompt = () =>\n `>>> CONTEXT: PREVIOUS SOURCE CONTENT <<<\\n\\`\\`\\`\\n` +\n (chunks[i - 1]?.content ?? '') +\n `\\n\\`\\`\\`\\n>>> END PREVIOUS CONTEXT <<<`;\n\n const getBaseChunkContextPrompt = () =>\n `>>> CONTEXT: NEXT CONTENT <<<\\n\\`\\`\\`\\n` +\n (chunks[i + 1]?.content ?? '') +\n `\\n\\`\\`\\`\\n>>> END NEXT CONTEXT <<<`;\n\n chunkLogger('Process started');\n\n const chunkTranslation = retryManager(async () => {\n const result = await chunkInference(\n [\n { role: 'system', content: basePrompt },\n ...(chunks[i + 1]\n ? [\n {\n role: 'system',\n content: getBaseChunkContextPrompt(),\n } as const,\n ]\n : []),\n ...(isFirstChunk\n ? []\n : [{ role: 'system', content: getPrevChunkPrompt() } as const]),\n {\n role: 'system',\n content: [\n `You are translating TARGET CHUNK (${i + 1}/${totalChunks}).`,\n `Translate ONLY the target chunk. Preserve frontmatter/code exactly.`,\n ].join('\\n'),\n },\n {\n role: 'user',\n content: `>>> TARGET CHUNK START <<<\\n${fileToTranslateCurrentChunk}\\n>>> TARGET CHUNK END <<<`,\n },\n ],\n aiOptions,\n configuration,\n aiClient,\n aiConfig\n );\n\n let processedChunk = sanitizeChunk(\n result?.fileContent,\n fileToTranslateCurrentChunk\n );\n processedChunk = fixChunkStartEndChars(\n processedChunk,\n fileToTranslateCurrentChunk\n );\n\n const isValid = validateTranslation(\n fileToTranslateCurrentChunk,\n processedChunk,\n chunkLogger\n );\n\n if (!isValid) {\n // Throwing an error here signals retryManager to try again\n throw new Error(\n `Validation failed for chunk ${i + 1}/${totalChunks}`\n );\n }\n\n return { content: processedChunk, tokens: result.tokenUsed };\n });\n\n const { content: translatedChunk, tokens } = await chunkTranslation();\n const chunkEndTime = performance.now();\n const chunkDuration = (chunkEndTime - chunkStartTime).toFixed(0);\n\n // Store Result\n translatedParts[i] = translatedChunk;\n\n if (onChunkReceive) {\n onChunkReceive(translatedChunk, i, totalChunks);\n }\n\n // Incremental Flush Strategy\n if (flushStrategy === 'incremental') {\n const isContiguous = translatedParts\n .slice(0, i + 1)\n .every((p) => p && p !== '');\n\n if (isContiguous) {\n let endIdx = 0;\n while (\n endIdx < totalChunks &&\n translatedParts[endIdx] &&\n translatedParts[endIdx] !== ''\n ) {\n endIdx++;\n }\n const currentContent = translatedParts.slice(0, endIdx).join('');\n // Write asynchronously/sync is fine here as node handles file locks reasonably well for single process\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, currentContent);\n }\n }\n\n chunkLogger(\n [\n `${colorizeNumber(tokens)} tokens used `,\n `${ANSIColors.GREY_DARK}in ${colorizeNumber(chunkDuration)}ms${ANSIColors.RESET}`,\n ].join('')\n );\n })\n );\n\n // Wait for all chunks for this specific file/locale to finish\n await Promise.all(tasks);\n\n // Final Flush\n const fullContent = translatedParts.join('');\n if (flushStrategy === 'end' || flushStrategy === 'incremental') {\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, fullContent);\n }\n\n const fileEndTime = performance.now();\n const totalDuration = ((fileEndTime - fileStartTime) / 1000).toFixed(2);\n const relativePath = relative(\n configuration.content.baseDir,\n outputFilePath\n );\n\n appLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(relativePath)} completed in ${colorizeNumber(totalDuration)}s.`\n );\n\n return fullContent;\n } catch (error: any) {\n errorState.count++;\n const errorMessage = error?.message ?? JSON.stringify(error);\n appLogger(`${colorize('✖', ANSIColors.RED)} Error: ${errorMessage}`);\n if (errorState.count >= errorState.maxErrors) errorState.shouldStop = true;\n return null;\n }\n};\n"],"mappings":"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"}
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/translateDoc/validation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Validates that the translated content matches the structure of the original.
|
|
4
|
+
* Throws an error if a mismatch is found, triggering a retry.
|
|
5
|
+
*/
|
|
6
|
+
const validateTranslation = (original, translated, logger) => {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (original.trimStart().startsWith("---")) {
|
|
9
|
+
if (!translated.trimStart().startsWith("---")) errors.push("YAML Frontmatter missing: Input starts with \"---\", output does not.");
|
|
10
|
+
const originalDashes = (original.match(/^---$/gm) || []).length;
|
|
11
|
+
const translatedDashes = (translated.match(/^---$/gm) || []).length;
|
|
12
|
+
if (originalDashes >= 2 && translatedDashes < 2) errors.push("YAML Frontmatter unclosed: Input has closing \"---\", output is missing it.");
|
|
13
|
+
}
|
|
14
|
+
const fenceRegex = /^\s*```/gm;
|
|
15
|
+
const originalFences = (original.match(fenceRegex) || []).length;
|
|
16
|
+
const translatedFences = (translated.match(fenceRegex) || []).length;
|
|
17
|
+
if (originalFences !== translatedFences) errors.push(`Code fence mismatch: Input has ${originalFences}, output has ${translatedFences}`);
|
|
18
|
+
const ratio = translated.length / (original.length || 1);
|
|
19
|
+
const isTooLong = ratio > 2.5;
|
|
20
|
+
const isSignificantLength = original.length > 50;
|
|
21
|
+
if (isTooLong && isSignificantLength) errors.push(`Length deviation: Output is ${translated.length} chars vs Input ${original.length} (${ratio.toFixed(1)}x). Likely included context.`);
|
|
22
|
+
const originalLines = original.split("\n").length;
|
|
23
|
+
const translatedLines = translated.split("\n").length;
|
|
24
|
+
if (originalLines > 5) {
|
|
25
|
+
if (translatedLines < originalLines * .4) errors.push(`Line count deviation: Output has ${translatedLines} lines, Input has ${originalLines}. Likely content deletion.`);
|
|
26
|
+
}
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
logger(`Validation Failed: ${errors.join(", ")}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Clean common AI artifacts
|
|
35
|
+
*/
|
|
36
|
+
const sanitizeChunk = (translated, original) => {
|
|
37
|
+
let cleaned = translated;
|
|
38
|
+
const match = cleaned.match(/^```(?:markdown|md|txt)?\n([\s\S]*?)\n```$/i);
|
|
39
|
+
if (match) cleaned = match[1];
|
|
40
|
+
if (!original.startsWith("\n") && cleaned.startsWith("\n")) cleaned = cleaned.replace(/^\n+/, "");
|
|
41
|
+
if (!original.startsWith(" ") && cleaned.startsWith(" ")) cleaned = cleaned.trimStart();
|
|
42
|
+
return cleaned;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { sanitizeChunk, validateTranslation };
|
|
47
|
+
//# sourceMappingURL=validation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.mjs","names":["errors: string[]"],"sources":["../../../src/translateDoc/validation.ts"],"sourcesContent":["import type { Logger } from '@intlayer/config';\n\n/**\n * Validates that the translated content matches the structure of the original.\n * Throws an error if a mismatch is found, triggering a retry.\n */\nexport const validateTranslation = (\n original: string,\n translated: string,\n logger: Logger\n): boolean => {\n const errors: string[] = [];\n\n // YAML Frontmatter Integrity (CRITICAL)\n if (original.trimStart().startsWith('---')) {\n if (!translated.trimStart().startsWith('---')) {\n errors.push(\n 'YAML Frontmatter missing: Input starts with \"---\", output does not.'\n );\n }\n const originalDashes = (original.match(/^---$/gm) || []).length;\n const translatedDashes = (translated.match(/^---$/gm) || []).length;\n if (originalDashes >= 2 && translatedDashes < 2) {\n errors.push(\n 'YAML Frontmatter unclosed: Input has closing \"---\", output is missing it.'\n );\n }\n }\n\n // Code Fence Check\n const fenceRegex = /^\\s*```/gm;\n const originalFences = (original.match(fenceRegex) || []).length;\n const translatedFences = (translated.match(fenceRegex) || []).length;\n\n if (originalFences !== translatedFences) {\n errors.push(\n `Code fence mismatch: Input has ${originalFences}, output has ${translatedFences}`\n );\n }\n\n // Length/Duplication Check\n const ratio = translated.length / (original.length || 1);\n const isTooLong = ratio > 2.5;\n const isSignificantLength = original.length > 50;\n\n if (isTooLong && isSignificantLength) {\n errors.push(\n `Length deviation: Output is ${translated.length} chars vs Input ${original.length} (${ratio.toFixed(1)}x). Likely included context.`\n );\n }\n\n // Line Count Heuristic\n const originalLines = original.split('\\n').length;\n const translatedLines = translated.split('\\n').length;\n\n if (originalLines > 5) {\n if (translatedLines < originalLines * 0.4) {\n errors.push(\n `Line count deviation: Output has ${translatedLines} lines, Input has ${originalLines}. Likely content deletion.`\n );\n }\n }\n\n if (errors.length > 0) {\n logger(`Validation Failed: ${errors.join(', ')}`);\n return false;\n }\n\n return true;\n};\n\n/**\n * Clean common AI artifacts\n */\nexport const sanitizeChunk = (translated: string, original: string): string => {\n let cleaned = translated;\n const wrapRegex = /^```(?:markdown|md|txt)?\\n([\\s\\S]*?)\\n```$/i;\n const match = cleaned.match(wrapRegex);\n if (match) cleaned = match[1];\n\n if (!original.startsWith('\\n') && cleaned.startsWith('\\n')) {\n cleaned = cleaned.replace(/^\\n+/, '');\n }\n if (!original.startsWith(' ') && cleaned.startsWith(' ')) {\n cleaned = cleaned.trimStart();\n }\n return cleaned;\n};\n"],"mappings":";;;;;AAMA,MAAa,uBACX,UACA,YACA,WACY;CACZ,MAAMA,SAAmB,EAAE;AAG3B,KAAI,SAAS,WAAW,CAAC,WAAW,MAAM,EAAE;AAC1C,MAAI,CAAC,WAAW,WAAW,CAAC,WAAW,MAAM,CAC3C,QAAO,KACL,wEACD;EAEH,MAAM,kBAAkB,SAAS,MAAM,UAAU,IAAI,EAAE,EAAE;EACzD,MAAM,oBAAoB,WAAW,MAAM,UAAU,IAAI,EAAE,EAAE;AAC7D,MAAI,kBAAkB,KAAK,mBAAmB,EAC5C,QAAO,KACL,8EACD;;CAKL,MAAM,aAAa;CACnB,MAAM,kBAAkB,SAAS,MAAM,WAAW,IAAI,EAAE,EAAE;CAC1D,MAAM,oBAAoB,WAAW,MAAM,WAAW,IAAI,EAAE,EAAE;AAE9D,KAAI,mBAAmB,iBACrB,QAAO,KACL,kCAAkC,eAAe,eAAe,mBACjE;CAIH,MAAM,QAAQ,WAAW,UAAU,SAAS,UAAU;CACtD,MAAM,YAAY,QAAQ;CAC1B,MAAM,sBAAsB,SAAS,SAAS;AAE9C,KAAI,aAAa,oBACf,QAAO,KACL,+BAA+B,WAAW,OAAO,kBAAkB,SAAS,OAAO,IAAI,MAAM,QAAQ,EAAE,CAAC,8BACzG;CAIH,MAAM,gBAAgB,SAAS,MAAM,KAAK,CAAC;CAC3C,MAAM,kBAAkB,WAAW,MAAM,KAAK,CAAC;AAE/C,KAAI,gBAAgB,GAClB;MAAI,kBAAkB,gBAAgB,GACpC,QAAO,KACL,oCAAoC,gBAAgB,oBAAoB,cAAc,4BACvF;;AAIL,KAAI,OAAO,SAAS,GAAG;AACrB,SAAO,sBAAsB,OAAO,KAAK,KAAK,GAAG;AACjD,SAAO;;AAGT,QAAO;;;;;AAMT,MAAa,iBAAiB,YAAoB,aAA6B;CAC7E,IAAI,UAAU;CAEd,MAAM,QAAQ,QAAQ,MADJ,8CACoB;AACtC,KAAI,MAAO,WAAU,MAAM;AAE3B,KAAI,CAAC,SAAS,WAAW,KAAK,IAAI,QAAQ,WAAW,KAAK,CACxD,WAAU,QAAQ,QAAQ,QAAQ,GAAG;AAEvC,KAAI,CAAC,SAAS,WAAW,IAAI,IAAI,QAAQ,WAAW,IAAI,CACtD,WAAU,QAAQ,WAAW;AAE/B,QAAO"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/translation-alignment/planActions.ts
|
|
2
|
-
const planAlignmentActions = (alignment, changedEnglishBlockIndexes
|
|
2
|
+
const planAlignmentActions = (alignment, changedEnglishBlockIndexes) => {
|
|
3
3
|
const actions = [];
|
|
4
4
|
const seenFrench = /* @__PURE__ */ new Set();
|
|
5
5
|
alignment.forEach((pair) => {
|
|
@@ -23,9 +23,7 @@ const planAlignmentActions = (alignment, changedEnglishBlockIndexes, similarityO
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (englishIndex >= 0 && frenchIndex !== null) {
|
|
26
|
-
|
|
27
|
-
const isHighSimilarity = pair.similarityScore >= similarityOptions.minimumMatchForReuse;
|
|
28
|
-
if (!isChanged && isHighSimilarity) actions.push({
|
|
26
|
+
if (!changedEnglishBlockIndexes.has(englishIndex)) actions.push({
|
|
29
27
|
kind: "reuse",
|
|
30
28
|
englishIndex,
|
|
31
29
|
frenchIndex
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planActions.mjs","names":["actions: PlannedAction[]"],"sources":["../../../src/translation-alignment/planActions.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"planActions.mjs","names":["actions: PlannedAction[]"],"sources":["../../../src/translation-alignment/planActions.ts"],"sourcesContent":["import type { AlignmentPair, AlignmentPlan, PlannedAction } from './types';\n\nexport const planAlignmentActions = (\n alignment: AlignmentPair[],\n changedEnglishBlockIndexes: Set<number>\n): AlignmentPlan => {\n const actions: PlannedAction[] = [];\n const seenFrench = new Set<number>();\n\n alignment.forEach((pair) => {\n const englishIndex = pair.englishIndex;\n const frenchIndex = pair.frenchIndex;\n\n // Case 1: Deletion (Exists in FR, not in EN)\n if (englishIndex === -1 && frenchIndex !== null) {\n if (!seenFrench.has(frenchIndex)) {\n actions.push({ kind: 'delete', frenchIndex });\n seenFrench.add(frenchIndex);\n }\n return;\n }\n\n // Case 2: New Insertion (Exists in EN, not in FR)\n if (englishIndex >= 0 && frenchIndex === null) {\n actions.push({ kind: 'insert_new', englishIndex });\n return;\n }\n\n // Case 3: Alignment (Exists in both)\n if (englishIndex >= 0 && frenchIndex !== null) {\n const isChanged = changedEnglishBlockIndexes.has(englishIndex);\n\n // If the block is NOT marked as changed by Git, we REUSE it.\n // We assume the existing translation is correct because the source hasn't been touched.\n // We ignore 'similarityScore' here because EN vs UK text will always have low similarity.\n if (!isChanged) {\n actions.push({ kind: 'reuse', englishIndex, frenchIndex });\n } else {\n // If the block IS changed, we normally Review.\n // OPTIONAL: You could add a check here for 'similarityScore > 0.99'\n // to detect whitespace-only changes, but generally, if Git says changed, we Review.\n actions.push({ kind: 'review', englishIndex, frenchIndex });\n }\n\n seenFrench.add(frenchIndex);\n return;\n }\n });\n\n return { actions };\n};\n"],"mappings":";AAEA,MAAa,wBACX,WACA,+BACkB;CAClB,MAAMA,UAA2B,EAAE;CACnC,MAAM,6BAAa,IAAI,KAAa;AAEpC,WAAU,SAAS,SAAS;EAC1B,MAAM,eAAe,KAAK;EAC1B,MAAM,cAAc,KAAK;AAGzB,MAAI,iBAAiB,MAAM,gBAAgB,MAAM;AAC/C,OAAI,CAAC,WAAW,IAAI,YAAY,EAAE;AAChC,YAAQ,KAAK;KAAE,MAAM;KAAU;KAAa,CAAC;AAC7C,eAAW,IAAI,YAAY;;AAE7B;;AAIF,MAAI,gBAAgB,KAAK,gBAAgB,MAAM;AAC7C,WAAQ,KAAK;IAAE,MAAM;IAAc;IAAc,CAAC;AAClD;;AAIF,MAAI,gBAAgB,KAAK,gBAAgB,MAAM;AAM7C,OAAI,CALc,2BAA2B,IAAI,aAAa,CAM5D,SAAQ,KAAK;IAAE,MAAM;IAAS;IAAc;IAAa,CAAC;OAK1D,SAAQ,KAAK;IAAE,MAAM;IAAU;IAAc;IAAa,CAAC;AAG7D,cAAW,IAAI,YAAY;AAC3B;;GAEF;AAEF,QAAO,EAAE,SAAS"}
|
|
@@ -2,127 +2,61 @@
|
|
|
2
2
|
const isBlankLine = (line) => line.trim().length === 0;
|
|
3
3
|
const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
|
|
4
4
|
const isHeading = (line) => /^\s*#{1,6}\s+/.test(line);
|
|
5
|
-
const
|
|
6
|
-
const isListItem = (line) => /^\s*([-*+]\s+|\d+\.[\t\s]+)/.test(line);
|
|
7
|
-
const isBlockquote = (line) => /^\s*>\s?/.test(line);
|
|
8
|
-
const isTableLike = (line) => /\|/.test(line) && !isCodeFenceStart(line);
|
|
9
|
-
const isCodeFenceStart = (line) => /^\s*```/.test(line);
|
|
5
|
+
const isFrontmatterDelimiter = (line) => /^\s*---\s*$/.test(line);
|
|
10
6
|
const trimTrailingNewlines = (text) => text.replace(/\n+$/g, "\n");
|
|
11
7
|
const segmentDocument = (text) => {
|
|
12
8
|
const lines = text.split("\n");
|
|
13
9
|
const blocks = [];
|
|
14
10
|
let index = 0;
|
|
11
|
+
let insideCodeBlock = false;
|
|
12
|
+
let currentSectionLines = [];
|
|
13
|
+
let currentSectionStartLine = 1;
|
|
14
|
+
const flushCurrentSection = (endIndex) => {
|
|
15
|
+
if (currentSectionLines.length > 0) {
|
|
16
|
+
const rawContent = currentSectionLines.join("\n");
|
|
17
|
+
if (rawContent.trim().length > 0) blocks.push({
|
|
18
|
+
type: "paragraph",
|
|
19
|
+
content: `${trimTrailingNewlines(rawContent)}\n`,
|
|
20
|
+
lineStart: currentSectionStartLine,
|
|
21
|
+
lineEnd: endIndex
|
|
22
|
+
});
|
|
23
|
+
currentSectionLines = [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
15
26
|
while (index < lines.length) {
|
|
16
|
-
const startIndex = index;
|
|
17
27
|
const currentLine = lines[index];
|
|
18
|
-
if (
|
|
28
|
+
if (blocks.length === 0 && isFrontmatterDelimiter(currentLine)) {
|
|
29
|
+
const startLine = index + 1;
|
|
19
30
|
const contentLines = [currentLine];
|
|
20
|
-
index
|
|
21
|
-
while (index < lines.length && !
|
|
22
|
-
contentLines.push(lines[index]);
|
|
23
|
-
index += 1;
|
|
24
|
-
}
|
|
25
|
-
if (index < lines.length) {
|
|
26
|
-
contentLines.push(lines[index]);
|
|
27
|
-
index += 1;
|
|
28
|
-
}
|
|
29
|
-
blocks.push({
|
|
30
|
-
type: "code_block",
|
|
31
|
-
content: `${trimTrailingNewlines(contentLines.join("\n"))}\n`,
|
|
32
|
-
lineStart: startIndex + 1,
|
|
33
|
-
lineEnd: index
|
|
34
|
-
});
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (isHorizontalRule(currentLine)) {
|
|
38
|
-
blocks.push({
|
|
39
|
-
type: "horizontal_rule",
|
|
40
|
-
content: `${currentLine}\n`,
|
|
41
|
-
lineStart: startIndex + 1,
|
|
42
|
-
lineEnd: startIndex + 1
|
|
43
|
-
});
|
|
44
|
-
index += 1;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (isHeading(currentLine)) {
|
|
48
|
-
blocks.push({
|
|
49
|
-
type: "heading",
|
|
50
|
-
content: `${currentLine}\n`,
|
|
51
|
-
lineStart: startIndex + 1,
|
|
52
|
-
lineEnd: startIndex + 1
|
|
53
|
-
});
|
|
54
|
-
index += 1;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (isListItem(currentLine)) {
|
|
58
|
-
const contentLines = [];
|
|
59
|
-
while (index < lines.length && (isListItem(lines[index]) || !isBlankLine(lines[index]) && /^\s{2,}/.test(lines[index]))) {
|
|
60
|
-
contentLines.push(lines[index]);
|
|
61
|
-
index += 1;
|
|
62
|
-
}
|
|
63
|
-
blocks.push({
|
|
64
|
-
type: "list_item",
|
|
65
|
-
content: `${trimTrailingNewlines(contentLines.join("\n"))}\n`,
|
|
66
|
-
lineStart: startIndex + 1,
|
|
67
|
-
lineEnd: index
|
|
68
|
-
});
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (isBlockquote(currentLine)) {
|
|
72
|
-
const contentLines = [];
|
|
73
|
-
while (index < lines.length && (isBlockquote(lines[index]) || !isBlankLine(lines[index]))) {
|
|
31
|
+
index++;
|
|
32
|
+
while (index < lines.length && !isFrontmatterDelimiter(lines[index])) {
|
|
74
33
|
contentLines.push(lines[index]);
|
|
75
|
-
index
|
|
34
|
+
index++;
|
|
76
35
|
}
|
|
77
|
-
|
|
78
|
-
type: "blockquote",
|
|
79
|
-
content: `${trimTrailingNewlines(contentLines.join("\n"))}\n`,
|
|
80
|
-
lineStart: startIndex + 1,
|
|
81
|
-
lineEnd: index
|
|
82
|
-
});
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (isTableLike(currentLine)) {
|
|
86
|
-
const contentLines = [];
|
|
87
|
-
while (index < lines.length && /\|/.test(lines[index]) && !isBlankLine(lines[index])) {
|
|
36
|
+
if (index < lines.length && isFrontmatterDelimiter(lines[index])) {
|
|
88
37
|
contentLines.push(lines[index]);
|
|
89
|
-
index
|
|
90
|
-
}
|
|
91
|
-
blocks.push({
|
|
92
|
-
type: "table",
|
|
93
|
-
content: `${trimTrailingNewlines(contentLines.join("\n"))}\n`,
|
|
94
|
-
lineStart: startIndex + 1,
|
|
95
|
-
lineEnd: index
|
|
96
|
-
});
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (!isBlankLine(currentLine)) {
|
|
100
|
-
const contentLines = [];
|
|
101
|
-
while (index < lines.length && !isBlankLine(lines[index])) {
|
|
102
|
-
if (isHeading(lines[index]) || isFencedCodeDelimiter(lines[index]) || isHorizontalRule(lines[index]) || isListItem(lines[index]) || isBlockquote(lines[index]) || isTableLike(lines[index])) break;
|
|
103
|
-
contentLines.push(lines[index]);
|
|
104
|
-
index += 1;
|
|
105
|
-
}
|
|
106
|
-
if (index < lines.length && isBlankLine(lines[index])) {
|
|
107
|
-
contentLines.push(lines[index]);
|
|
108
|
-
index += 1;
|
|
38
|
+
index++;
|
|
109
39
|
}
|
|
110
40
|
blocks.push({
|
|
111
41
|
type: "paragraph",
|
|
112
42
|
content: `${trimTrailingNewlines(contentLines.join("\n"))}\n`,
|
|
113
|
-
lineStart:
|
|
43
|
+
lineStart: startLine,
|
|
114
44
|
lineEnd: index
|
|
115
45
|
});
|
|
116
46
|
continue;
|
|
117
47
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
48
|
+
if (isFencedCodeDelimiter(currentLine)) insideCodeBlock = !insideCodeBlock;
|
|
49
|
+
if (!insideCodeBlock && isHeading(currentLine)) {
|
|
50
|
+
if (currentSectionLines.length > 0) flushCurrentSection(index);
|
|
51
|
+
currentSectionStartLine = index + 1;
|
|
52
|
+
currentSectionLines = [currentLine];
|
|
53
|
+
} else {
|
|
54
|
+
if (currentSectionLines.length === 0 && !isBlankLine(currentLine)) currentSectionStartLine = index + 1;
|
|
55
|
+
currentSectionLines.push(currentLine);
|
|
56
|
+
}
|
|
57
|
+
index++;
|
|
125
58
|
}
|
|
59
|
+
flushCurrentSection(index);
|
|
126
60
|
return blocks;
|
|
127
61
|
};
|
|
128
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmentDocument.mjs","names":["blocks: Block[]","contentLines: string[]"],"sources":["../../../src/translation-alignment/segmentDocument.ts"],"sourcesContent":["import type { Block } from './types';\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\
|
|
1
|
+
{"version":3,"file":"segmentDocument.mjs","names":["blocks: Block[]","currentSectionLines: string[]","contentLines: string[]"],"sources":["../../../src/translation-alignment/segmentDocument.ts"],"sourcesContent":["import type { Block } from './types';\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\nconst isHeading = (line: string): boolean => /^\\s*#{1,6}\\s+/.test(line);\nconst isFrontmatterDelimiter = (line: string): boolean =>\n /^\\s*---\\s*$/.test(line);\nconst trimTrailingNewlines = (text: string): string =>\n text.replace(/\\n+$/g, '\\n');\n\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const blocks: Block[] = [];\n\n let index = 0;\n let insideCodeBlock = false;\n\n // Buffers\n let currentSectionLines: string[] = [];\n let currentSectionStartLine = 1;\n\n const flushCurrentSection = (endIndex: number) => {\n if (currentSectionLines.length > 0) {\n // Filter out leading blank lines from the block content to keep it clean,\n // but strictly speaking, we just want to ensure non-empty content.\n const rawContent = currentSectionLines.join('\\n');\n\n if (rawContent.trim().length > 0) {\n blocks.push({\n type: 'paragraph', // Generic type\n content: `${trimTrailingNewlines(rawContent)}\\n`,\n lineStart: currentSectionStartLine,\n lineEnd: endIndex,\n });\n }\n currentSectionLines = [];\n }\n };\n\n while (index < lines.length) {\n const currentLine = lines[index];\n\n // 1. Handle Frontmatter (Must be at start of file)\n if (blocks.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startLine = index + 1;\n const contentLines: string[] = [currentLine];\n index++;\n\n while (index < lines.length && !isFrontmatterDelimiter(lines[index])) {\n contentLines.push(lines[index]);\n index++;\n }\n\n if (index < lines.length && isFrontmatterDelimiter(lines[index])) {\n contentLines.push(lines[index]);\n index++;\n }\n\n blocks.push({\n type: 'paragraph',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startLine,\n lineEnd: index,\n });\n continue;\n }\n\n // 2. Track Code Blocks (Headers inside code blocks are ignored)\n if (isFencedCodeDelimiter(currentLine)) {\n insideCodeBlock = !insideCodeBlock;\n }\n\n const isHeader = !insideCodeBlock && isHeading(currentLine);\n\n // 3. Split on Headers\n if (isHeader) {\n // If we have accumulated content, flush it as the previous block\n if (currentSectionLines.length > 0) {\n flushCurrentSection(index);\n }\n // Start a new section with this header\n currentSectionStartLine = index + 1;\n currentSectionLines = [currentLine];\n } else {\n // Accumulate content\n if (currentSectionLines.length === 0 && !isBlankLine(currentLine)) {\n currentSectionStartLine = index + 1;\n }\n currentSectionLines.push(currentLine);\n }\n\n index++;\n }\n\n // Flush remaining content\n flushCurrentSection(index);\n\n return blocks;\n};\n"],"mappings":";AAEA,MAAM,eAAe,SAA0B,KAAK,MAAM,CAAC,WAAW;AACtE,MAAM,yBAAyB,SAA0B,UAAU,KAAK,KAAK;AAC7E,MAAM,aAAa,SAA0B,gBAAgB,KAAK,KAAK;AACvE,MAAM,0BAA0B,SAC9B,cAAc,KAAK,KAAK;AAC1B,MAAM,wBAAwB,SAC5B,KAAK,QAAQ,SAAS,KAAK;AAE7B,MAAa,mBAAmB,SAA0B;CACxD,MAAM,QAAQ,KAAK,MAAM,KAAK;CAC9B,MAAMA,SAAkB,EAAE;CAE1B,IAAI,QAAQ;CACZ,IAAI,kBAAkB;CAGtB,IAAIC,sBAAgC,EAAE;CACtC,IAAI,0BAA0B;CAE9B,MAAM,uBAAuB,aAAqB;AAChD,MAAI,oBAAoB,SAAS,GAAG;GAGlC,MAAM,aAAa,oBAAoB,KAAK,KAAK;AAEjD,OAAI,WAAW,MAAM,CAAC,SAAS,EAC7B,QAAO,KAAK;IACV,MAAM;IACN,SAAS,GAAG,qBAAqB,WAAW,CAAC;IAC7C,WAAW;IACX,SAAS;IACV,CAAC;AAEJ,yBAAsB,EAAE;;;AAI5B,QAAO,QAAQ,MAAM,QAAQ;EAC3B,MAAM,cAAc,MAAM;AAG1B,MAAI,OAAO,WAAW,KAAK,uBAAuB,YAAY,EAAE;GAC9D,MAAM,YAAY,QAAQ;GAC1B,MAAMC,eAAyB,CAAC,YAAY;AAC5C;AAEA,UAAO,QAAQ,MAAM,UAAU,CAAC,uBAAuB,MAAM,OAAO,EAAE;AACpE,iBAAa,KAAK,MAAM,OAAO;AAC/B;;AAGF,OAAI,QAAQ,MAAM,UAAU,uBAAuB,MAAM,OAAO,EAAE;AAChE,iBAAa,KAAK,MAAM,OAAO;AAC/B;;AAGF,UAAO,KAAK;IACV,MAAM;IACN,SAAS,GAAG,qBAAqB,aAAa,KAAK,KAAK,CAAC,CAAC;IAC1D,WAAW;IACX,SAAS;IACV,CAAC;AACF;;AAIF,MAAI,sBAAsB,YAAY,CACpC,mBAAkB,CAAC;AAMrB,MAHiB,CAAC,mBAAmB,UAAU,YAAY,EAG7C;AAEZ,OAAI,oBAAoB,SAAS,EAC/B,qBAAoB,MAAM;AAG5B,6BAA0B,QAAQ;AAClC,yBAAsB,CAAC,YAAY;SAC9B;AAEL,OAAI,oBAAoB,WAAW,KAAK,CAAC,YAAY,YAAY,CAC/D,2BAA0B,QAAQ;AAEpC,uBAAoB,KAAK,YAAY;;AAGvC;;AAIF,qBAAoB,MAAM;AAE1B,QAAO"}
|
package/dist/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/cli.ts"],"sourcesContent":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/cli.ts"],"sourcesContent":[],"mappings":";;;cAoCa;KAwIR,UAAA;EAxIQ,MAAA,CAAA,EAAA,MAEA;EAsIR,OAAA,CAAA,EAAA,OAAU;AAKf,CAAA;AA2Da,KA3DD,oBAAA,GA2tBX;;;;;IAttBG;;;;;;;;;cAsDS,cAAa"}
|