Skip to main content
Glama
reviewDocBlockAware.ts6.36 kB
import { mkdirSync, writeFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { readAsset } from 'utils:asset'; import type { AIConfig } from '@intlayer/ai'; import type { AIOptions } from '@intlayer/api'; import { formatLocale, formatPath } from '@intlayer/chokidar'; import { ANSIColors, colon, colorize, colorizeNumber, type GetConfigurationOptions, getAppLogger, getConfiguration, retryManager, } from '@intlayer/config'; import { getLocaleName } from '@intlayer/core'; import { type Locale, Locales } from '@intlayer/types'; import { buildAlignmentPlan, mergeReviewedSegments, } from './translation-alignment/pipeline'; import { chunkInference } from './utils/chunkInference'; import { fixChunkStartEndChars } from './utils/fixChunkStartEndChars'; import type { AIClient } from './utils/setupAI'; /** * Review a file using block-aware alignment. * This approach: * 1. Segments both English and French documents into semantic blocks * 2. Aligns blocks using structure (special chars, numbers) and context * 3. Detects which blocks changed, were added, or deleted * 4. Only sends changed/new blocks to AI for translation * 5. Handles reordering automatically */ export const reviewFileBlockAware = async ( baseFilePath: string, outputFilePath: string, locale: Locale, baseLocale: Locale, aiOptions?: AIOptions, configOptions?: GetConfigurationOptions, customInstructions?: string, changedLines?: number[], aiClient?: AIClient, aiConfig?: AIConfig ) => { const configuration = getConfiguration(configOptions); const applicationLogger = getAppLogger(configuration); const englishText = await readFile(baseFilePath, 'utf-8'); const frenchText = await readFile(outputFilePath, 'utf-8').catch(() => ''); const basePrompt = readAsset('./prompts/REVIEW_PROMPT.md', 'utf-8') .replaceAll('{{localeName}}', `${formatLocale(locale, false)}`) .replaceAll('{{baseLocaleName}}', `${formatLocale(baseLocale, false)}`) .replace('{{applicationContext}}', aiOptions?.applicationContext ?? '-') .replace('{{customInstructions}}', customInstructions ?? '-'); const filePrefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `; const filePrefix = [ colon(filePrefixText, { colSize: 40 }), `→ ${ANSIColors.RESET}`, ].join(''); const prefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `; const prefix = [ colon(prefixText, { colSize: 40 }), `→ ${ANSIColors.RESET}`, ].join(''); // Build block-aware alignment and plan const { englishBlocks, frenchBlocks, plan, segmentsToReview } = buildAlignmentPlan({ englishText, frenchText, changedLines, }); applicationLogger( `${filePrefix}Block-aware alignment complete. Total blocks: EN=${colorizeNumber(englishBlocks.length)}, FR=${colorizeNumber(frenchBlocks.length)}` ); applicationLogger( `${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)}` ); if (segmentsToReview.length === 0) { applicationLogger( `${filePrefix}No segments need review, reusing existing translation` ); mkdirSync(dirname(outputFilePath), { recursive: true }); writeFileSync( outputFilePath, mergeReviewedSegments(plan, frenchBlocks, new Map()) ); applicationLogger( `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no changes needed).` ); return; } applicationLogger( `${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)}` ); // Review segments that need AI translation const reviewedSegmentsMap = new Map<number, string>(); for (const segment of segmentsToReview) { const segmentNumber = segmentsToReview.indexOf(segment) + 1; const englishBlock = segment.englishBlock; const getBaseChunkContextPrompt = () => `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\n` + `///chunksStart///\n` + englishBlock.content + `///chunksEnd///`; const getFrenchChunkPrompt = () => `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\n` + `///chunksStart///\n` + (segment.frenchBlockText ?? '') + `///chunksEnd///`; const reviewedChunkResult = await retryManager(async () => { const result = await chunkInference( [ { role: 'system', content: basePrompt }, { role: 'system', content: getBaseChunkContextPrompt() }, { role: 'system', content: getFrenchChunkPrompt() }, { role: 'system', 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}).`, }, { role: 'user', content: englishBlock.content }, ], aiOptions, configuration, aiClient, aiConfig ); applicationLogger( `${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}` ); const fixed = fixChunkStartEndChars( result?.fileContent, englishBlock.content ); return fixed; })(); reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult); } // Merge reviewed segments back into final document const finalFrenchOutput = mergeReviewedSegments( plan, frenchBlocks, reviewedSegmentsMap ); mkdirSync(dirname(outputFilePath), { recursive: true }); writeFileSync(outputFilePath, finalFrenchOutput); applicationLogger( `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.` ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server