Skip to main content
Glama
fill.ts6.16 kB
import { basename, relative } from 'node:path'; import type { AIOptions } from '@intlayer/api'; import { formatPath, getGlobalLimiter, getTaskLimiter, type ListGitFilesOptions, prepareIntlayer, writeContentDeclaration, } from '@intlayer/chokidar'; import { ANSIColors, colorize, colorizeKey, colorizePath, getAppLogger, getConfiguration, } from '@intlayer/config'; import type { Locale } from '@intlayer/types'; import { ensureArray, type GetTargetDictionaryOptions, getTargetUnmergedDictionaries, } from '../getTargetDictionary'; import { setupAI } from '../utils/setupAI'; import { listTranslationsTasks, type TranslationTask, } from './listTranslationsTasks'; import { translateDictionary } from './translateDictionary'; import { writeFill } from './writeFill'; const NB_CONCURRENT_TRANSLATIONS = 7; // Arguments for the fill function export type FillOptions = { sourceLocale?: Locale; outputLocales?: Locale | Locale[]; mode?: 'complete' | 'review'; gitOptions?: ListGitFilesOptions; aiOptions?: AIOptions; // Added aiOptions to be passed to translateJSON verbose?: boolean; nbConcurrentTranslations?: number; nbConcurrentTasks?: number; // NEW: number of tasks that may run at once build?: boolean; skipMetadata?: boolean; } & GetTargetDictionaryOptions; /** * Fill translations based on the provided options. */ export const fill = async (options?: FillOptions): Promise<void> => { const configuration = getConfiguration(options?.configOptions); const appLogger = getAppLogger(configuration); if (options?.build === true) { await prepareIntlayer(configuration, { forceRun: true }); } else if (typeof options?.build === 'undefined') { await prepareIntlayer(configuration); } const { defaultLocale, locales } = configuration.internationalization; const mode = options?.mode ?? 'complete'; const baseLocale = options?.sourceLocale ?? defaultLocale; const outputLocales = options?.outputLocales ? ensureArray(options.outputLocales) : locales; const aiResult = await setupAI(configuration, options?.aiOptions); if (!aiResult?.hasAIAccess) return; const { aiClient, aiConfig } = aiResult; const targetUnmergedDictionaries = await getTargetUnmergedDictionaries(options); const affectedDictionaryKeys = new Set<string>(); targetUnmergedDictionaries.forEach((dict) => { affectedDictionaryKeys.add(dict.key); }); const keysToProcess = Array.from(affectedDictionaryKeys); appLogger([ 'Affected dictionary keys for processing:', keysToProcess.length > 0 ? keysToProcess.map((key) => colorizeKey(key)).join(', ') : colorize('No keys found', ANSIColors.YELLOW), ]); if (keysToProcess.length === 0) return; /** * List the translations tasks * * Create a list of per-locale dictionaries to translate * * In 'complete' mode, filter only the missing locales to translate */ const translationTasks: TranslationTask[] = listTranslationsTasks( targetUnmergedDictionaries.map((dictionary) => dictionary.localId!), outputLocales, mode, baseLocale, configuration ); // AI calls in flight at once (translateJSON + metadata audit) const nbConcurrentTranslations = options?.nbConcurrentTranslations ?? NB_CONCURRENT_TRANSLATIONS; const globalLimiter = getGlobalLimiter(nbConcurrentTranslations); // NEW: number of *tasks* that may run at once (start/prepare/log/write) const nbConcurrentTasks = Math.max( 1, Math.min( options?.nbConcurrentTasks ?? nbConcurrentTranslations, translationTasks.length ) ); const taskLimiter = getTaskLimiter(nbConcurrentTasks); const runners = translationTasks.map((task) => taskLimiter(async () => { const relativePath = relative( configuration?.content?.baseDir ?? process.cwd(), task?.dictionaryFilePath ?? '' ); // log AFTER acquiring a task slot appLogger( `${task.dictionaryPreset} Processing ${colorizePath(basename(relativePath))}`, { level: 'info' } ); const translationTaskResult = await translateDictionary( task, configuration, { mode, aiOptions: options?.aiOptions, fillMetadata: !options?.skipMetadata, onHandle: globalLimiter, // <= AI calls go through here aiClient, aiConfig, } ); if (!translationTaskResult?.dictionaryOutput) return; const { dictionaryOutput, sourceLocale } = translationTaskResult; // Determine if we should write to separate files // - If dictionary has explicit fill setting (string or object), use it // - If dictionary is per-locale AND has no explicit fill=false, use global fill config // - If dictionary is multilingual (no locale property), always write to same file const hasDictionaryLevelFill = typeof dictionaryOutput.fill === 'string' || typeof dictionaryOutput.fill === 'object'; const isPerLocale = typeof dictionaryOutput.locale === 'string'; const effectiveFill = hasDictionaryLevelFill ? dictionaryOutput.fill : isPerLocale ? (configuration.dictionary?.fill ?? true) : false; // Multilingual dictionaries don't use fill by default const isFillOtherFile = typeof effectiveFill === 'string' || typeof effectiveFill === 'object'; if (isFillOtherFile) { await writeFill( { ...dictionaryOutput, // Ensure fill is set on the dictionary for writeFill to use fill: effectiveFill, }, outputLocales, [sourceLocale], configuration ); } else { await writeContentDeclaration(dictionaryOutput, configuration); if (dictionaryOutput.filePath) { appLogger( `${task.dictionaryPreset} Content declaration written to ${formatPath(basename(dictionaryOutput.filePath))}`, { level: 'info' } ); } } }) ); await Promise.all(runners); await (globalLimiter as any).onIdle(); };

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