@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.
Files changed (70) hide show
  1. package/dist/cjs/cli.cjs +4 -4
  2. package/dist/cjs/cli.cjs.map +1 -1
  3. package/dist/cjs/index.cjs +4 -5
  4. package/dist/cjs/{reviewDoc.cjs → reviewDoc/reviewDoc.cjs} +17 -15
  5. package/dist/cjs/reviewDoc/reviewDoc.cjs.map +1 -0
  6. package/dist/cjs/{reviewDocBlockAware.cjs → reviewDoc/reviewDocBlockAware.cjs} +12 -8
  7. package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs.map +1 -0
  8. package/dist/cjs/translateDoc/index.cjs +8 -0
  9. package/dist/cjs/translateDoc/translateDoc.cjs +74 -0
  10. package/dist/cjs/translateDoc/translateDoc.cjs.map +1 -0
  11. package/dist/cjs/translateDoc/translateFile.cjs +103 -0
  12. package/dist/cjs/translateDoc/translateFile.cjs.map +1 -0
  13. package/dist/cjs/translateDoc/types.cjs +0 -0
  14. package/dist/cjs/translateDoc/validation.cjs +49 -0
  15. package/dist/cjs/translateDoc/validation.cjs.map +1 -0
  16. package/dist/cjs/translation-alignment/planActions.cjs +2 -4
  17. package/dist/cjs/translation-alignment/planActions.cjs.map +1 -1
  18. package/dist/cjs/translation-alignment/segmentDocument.cjs +35 -101
  19. package/dist/cjs/translation-alignment/segmentDocument.cjs.map +1 -1
  20. package/dist/esm/cli.mjs +2 -2
  21. package/dist/esm/cli.mjs.map +1 -1
  22. package/dist/esm/index.mjs +3 -3
  23. package/dist/esm/{reviewDoc.mjs → reviewDoc/reviewDoc.mjs} +14 -12
  24. package/dist/esm/reviewDoc/reviewDoc.mjs.map +1 -0
  25. package/dist/esm/{reviewDocBlockAware.mjs → reviewDoc/reviewDocBlockAware.mjs} +11 -7
  26. package/dist/esm/reviewDoc/reviewDocBlockAware.mjs.map +1 -0
  27. package/dist/esm/translateDoc/index.mjs +5 -0
  28. package/dist/esm/translateDoc/translateDoc.mjs +72 -0
  29. package/dist/esm/translateDoc/translateDoc.mjs.map +1 -0
  30. package/dist/esm/translateDoc/translateFile.mjs +102 -0
  31. package/dist/esm/translateDoc/translateFile.mjs.map +1 -0
  32. package/dist/esm/translateDoc/types.mjs +0 -0
  33. package/dist/esm/translateDoc/validation.mjs +47 -0
  34. package/dist/esm/translateDoc/validation.mjs.map +1 -0
  35. package/dist/esm/translation-alignment/planActions.mjs +2 -4
  36. package/dist/esm/translation-alignment/planActions.mjs.map +1 -1
  37. package/dist/esm/translation-alignment/segmentDocument.mjs +35 -101
  38. package/dist/esm/translation-alignment/segmentDocument.mjs.map +1 -1
  39. package/dist/types/cli.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +3 -3
  41. package/dist/types/{reviewDoc.d.ts → reviewDoc/reviewDoc.d.ts} +1 -1
  42. package/dist/types/reviewDoc/reviewDoc.d.ts.map +1 -0
  43. package/dist/types/{reviewDocBlockAware.d.ts → reviewDoc/reviewDocBlockAware.d.ts} +2 -2
  44. package/dist/types/reviewDoc/reviewDocBlockAware.d.ts.map +1 -0
  45. package/dist/types/translateDoc/index.d.ts +5 -0
  46. package/dist/types/translateDoc/translateDoc.d.ts +21 -0
  47. package/dist/types/translateDoc/translateDoc.d.ts.map +1 -0
  48. package/dist/types/translateDoc/translateFile.d.ts +21 -0
  49. package/dist/types/translateDoc/translateFile.d.ts.map +1 -0
  50. package/dist/types/translateDoc/types.d.ts +47 -0
  51. package/dist/types/translateDoc/types.d.ts.map +1 -0
  52. package/dist/types/translateDoc/validation.d.ts +16 -0
  53. package/dist/types/translateDoc/validation.d.ts.map +1 -0
  54. package/dist/types/translation-alignment/planActions.d.ts +2 -2
  55. package/dist/types/translation-alignment/planActions.d.ts.map +1 -1
  56. package/dist/types/translation-alignment/rebuildDocument.d.ts.map +1 -1
  57. package/dist/types/translation-alignment/segmentDocument.d.ts.map +1 -1
  58. package/package.json +11 -11
  59. package/dist/cjs/reviewDoc.cjs.map +0 -1
  60. package/dist/cjs/reviewDocBlockAware.cjs.map +0 -1
  61. package/dist/cjs/translateDoc.cjs +0 -163
  62. package/dist/cjs/translateDoc.cjs.map +0 -1
  63. package/dist/esm/reviewDoc.mjs.map +0 -1
  64. package/dist/esm/reviewDocBlockAware.mjs.map +0 -1
  65. package/dist/esm/translateDoc.mjs +0 -160
  66. package/dist/esm/translateDoc.mjs.map +0 -1
  67. package/dist/types/reviewDoc.d.ts.map +0 -1
  68. package/dist/types/reviewDocBlockAware.d.ts.map +0 -1
  69. package/dist/types/translateDoc.d.ts +0 -56
  70. 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 "./_virtual/_utils_asset.mjs";
2
- import { mergeReviewedSegments } from "./translation-alignment/rebuildDocument.mjs";
3
- import { buildAlignmentPlan } from "./translation-alignment/pipeline.mjs";
4
- import { chunkInference } from "./utils/chunkInference.mjs";
5
- import { fixChunkStartEndChars } from "./utils/fixChunkStartEndChars.mjs";
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
- return fixChunkStartEndChars(result?.fileContent, englishBlock.content);
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,5 @@
1
+ import { sanitizeChunk, validateTranslation } from "./validation.mjs";
2
+ import { translateFile } from "./translateFile.mjs";
3
+ import { translateDoc } from "./translateDoc.mjs";
4
+
5
+ export { sanitizeChunk, translateDoc, translateFile, validateTranslation };
@@ -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, similarityOptions) => {
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
- const isChanged = changedEnglishBlockIndexes.has(englishIndex);
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 {\n AlignmentPair,\n AlignmentPlan,\n PlannedAction,\n SimilarityOptions,\n} from './types';\n\nexport const planAlignmentActions = (\n alignment: AlignmentPair[],\n changedEnglishBlockIndexes: Set<number>,\n similarityOptions: SimilarityOptions\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 if (englishIndex === -1 && frenchIndex !== null) {\n // french only -> delete\n if (!seenFrench.has(frenchIndex)) {\n actions.push({ kind: 'delete', frenchIndex });\n seenFrench.add(frenchIndex);\n }\n return;\n }\n\n if (englishIndex >= 0 && frenchIndex === null) {\n // new english block\n actions.push({ kind: 'insert_new', englishIndex });\n return;\n }\n\n if (englishIndex >= 0 && frenchIndex !== null) {\n // matched pair\n const isChanged = changedEnglishBlockIndexes.has(englishIndex);\n const isHighSimilarity =\n pair.similarityScore >= similarityOptions.minimumMatchForReuse;\n\n if (!isChanged && isHighSimilarity) {\n actions.push({ kind: 'reuse', englishIndex, frenchIndex });\n } else {\n actions.push({ kind: 'review', englishIndex, frenchIndex });\n }\n seenFrench.add(frenchIndex);\n return;\n }\n });\n\n return { actions };\n};\n"],"mappings":";AAOA,MAAa,wBACX,WACA,4BACA,sBACkB;CAClB,MAAMA,UAA2B,EAAE;CACnC,MAAM,6BAAa,IAAI,KAAa;AAEpC,WAAU,SAAS,SAAS;EAC1B,MAAM,eAAe,KAAK;EAC1B,MAAM,cAAc,KAAK;AAEzB,MAAI,iBAAiB,MAAM,gBAAgB,MAAM;AAE/C,OAAI,CAAC,WAAW,IAAI,YAAY,EAAE;AAChC,YAAQ,KAAK;KAAE,MAAM;KAAU;KAAa,CAAC;AAC7C,eAAW,IAAI,YAAY;;AAE7B;;AAGF,MAAI,gBAAgB,KAAK,gBAAgB,MAAM;AAE7C,WAAQ,KAAK;IAAE,MAAM;IAAc;IAAc,CAAC;AAClD;;AAGF,MAAI,gBAAgB,KAAK,gBAAgB,MAAM;GAE7C,MAAM,YAAY,2BAA2B,IAAI,aAAa;GAC9D,MAAM,mBACJ,KAAK,mBAAmB,kBAAkB;AAE5C,OAAI,CAAC,aAAa,iBAChB,SAAQ,KAAK;IAAE,MAAM;IAAS;IAAc;IAAa,CAAC;OAE1D,SAAQ,KAAK;IAAE,MAAM;IAAU;IAAc;IAAa,CAAC;AAE7D,cAAW,IAAI,YAAY;AAC3B;;GAEF;AAEF,QAAO,EAAE,SAAS"}
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 isHorizontalRule = (line) => /^(\s*[-*_]){3,}\s*$/.test(line);
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 (isFencedCodeDelimiter(currentLine)) {
28
+ if (blocks.length === 0 && isFrontmatterDelimiter(currentLine)) {
29
+ const startLine = index + 1;
19
30
  const contentLines = [currentLine];
20
- index += 1;
21
- while (index < lines.length && !isFencedCodeDelimiter(lines[index])) {
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 += 1;
34
+ index++;
76
35
  }
77
- blocks.push({
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 += 1;
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: startIndex + 1,
43
+ lineStart: startLine,
114
44
  lineEnd: index
115
45
  });
116
46
  continue;
117
47
  }
118
- blocks.push({
119
- type: "unknown",
120
- content: `${currentLine}\n`,
121
- lineStart: startIndex + 1,
122
- lineEnd: startIndex + 1
123
- });
124
- index += 1;
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;\n\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\n\nconst isHeading = (line: string): boolean => /^\\s*#{1,6}\\s+/.test(line);\n\nconst isHorizontalRule = (line: string): boolean =>\n /^(\\s*[-*_]){3,}\\s*$/.test(line);\n\nconst isListItem = (line: string): boolean =>\n /^\\s*([-*+]\\s+|\\d+\\.[\\t\\s]+)/.test(line);\n\nconst isBlockquote = (line: string): boolean => /^\\s*>\\s?/.test(line);\n\nconst isTableLike = (line: string): boolean =>\n /\\|/.test(line) && !isCodeFenceStart(line);\n\nconst isCodeFenceStart = (line: string): boolean => /^\\s*```/.test(line);\n\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 while (index < lines.length) {\n const startIndex = index;\n const currentLine = lines[index];\n\n // Code block (fenced)\n if (isFencedCodeDelimiter(currentLine)) {\n const contentLines: string[] = [currentLine];\n index += 1;\n while (index < lines.length && !isFencedCodeDelimiter(lines[index])) {\n contentLines.push(lines[index]);\n index += 1;\n }\n if (index < lines.length) {\n contentLines.push(lines[index]);\n index += 1;\n }\n blocks.push({\n type: 'code_block',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startIndex + 1,\n lineEnd: index,\n });\n continue;\n }\n\n // Horizontal rule\n if (isHorizontalRule(currentLine)) {\n blocks.push({\n type: 'horizontal_rule',\n content: `${currentLine}\\n`,\n lineStart: startIndex + 1,\n lineEnd: startIndex + 1,\n });\n index += 1;\n continue;\n }\n\n // Heading\n if (isHeading(currentLine)) {\n blocks.push({\n type: 'heading',\n content: `${currentLine}\\n`,\n lineStart: startIndex + 1,\n lineEnd: startIndex + 1,\n });\n index += 1;\n continue;\n }\n\n // List block (one or more consecutive list items)\n if (isListItem(currentLine)) {\n const contentLines: string[] = [];\n while (\n index < lines.length &&\n (isListItem(lines[index]) ||\n (!isBlankLine(lines[index]) && /^\\s{2,}/.test(lines[index])))\n ) {\n contentLines.push(lines[index]);\n index += 1;\n }\n blocks.push({\n type: 'list_item',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startIndex + 1,\n lineEnd: index,\n });\n continue;\n }\n\n // Blockquote (may span multiple lines)\n if (isBlockquote(currentLine)) {\n const contentLines: string[] = [];\n while (\n index < lines.length &&\n (isBlockquote(lines[index]) || !isBlankLine(lines[index]))\n ) {\n contentLines.push(lines[index]);\n index += 1;\n }\n blocks.push({\n type: 'blockquote',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startIndex + 1,\n lineEnd: index,\n });\n continue;\n }\n\n // Table-like (simple heuristic)\n if (isTableLike(currentLine)) {\n const contentLines: string[] = [];\n while (\n index < lines.length &&\n /\\|/.test(lines[index]) &&\n !isBlankLine(lines[index])\n ) {\n contentLines.push(lines[index]);\n index += 1;\n }\n blocks.push({\n type: 'table',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startIndex + 1,\n lineEnd: index,\n });\n continue;\n }\n\n // Paragraph (gathers until blank line)\n if (!isBlankLine(currentLine)) {\n const contentLines: string[] = [];\n while (index < lines.length && !isBlankLine(lines[index])) {\n // stop if we detect a new structural block start\n if (\n isHeading(lines[index]) ||\n isFencedCodeDelimiter(lines[index]) ||\n isHorizontalRule(lines[index]) ||\n isListItem(lines[index]) ||\n isBlockquote(lines[index]) ||\n isTableLike(lines[index])\n ) {\n break;\n }\n contentLines.push(lines[index]);\n index += 1;\n }\n // consume a single trailing blank line if present\n if (index < lines.length && isBlankLine(lines[index])) {\n contentLines.push(lines[index]);\n index += 1;\n }\n blocks.push({\n type: 'paragraph',\n content: `${trimTrailingNewlines(contentLines.join('\\n'))}\\n`,\n lineStart: startIndex + 1,\n lineEnd: index,\n });\n continue;\n }\n\n // Blank line outside of a paragraph: keep to preserve spacing minimally\n blocks.push({\n type: 'unknown',\n content: `${currentLine}\\n`,\n lineStart: startIndex + 1,\n lineEnd: startIndex + 1,\n });\n index += 1;\n }\n\n return blocks;\n};\n"],"mappings":"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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/cli.ts"],"sourcesContent":[],"mappings":";;;cAqCa;KAwIR,UAAA;EAxIQ,MAAA,CAAA,EAAA,MAEA;EAsIR,OAAA,CAAA,EAAA,OAAU;AAKf,CAAA;AA2Da,KA3DD,oBAAA,GA2tBX;;;;;IAttBG;;;;;;;;;cAsDS,cAAa"}
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"}