@intlayer/cli 8.9.0 → 8.9.2

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.
@@ -40,7 +40,7 @@ const fill = async (options) => {
40
40
  }
41
41
  }
42
42
  const targetUnmergedDictionaries = await require_getTargetDictionary.getTargetUnmergedDictionaries(options);
43
- const sourceDictionaries = await (0, _intlayer_chokidar_build.loadContentDeclarations)([...new Set(targetUnmergedDictionaries.filter((unmergedDictionary) => unmergedDictionary.location !== "remote").map((unmergedDictionary) => unmergedDictionary.filePath).filter(Boolean))].map((sourcePath) => (0, node_path.join)(configuration.system.baseDir, sourcePath)), configuration, void 0, { logError: false });
43
+ const sourceDictionaries = await (0, _intlayer_chokidar_build.loadContentDeclarations)([...new Set(targetUnmergedDictionaries.filter((unmergedDictionary) => ["local", "hybrid"].includes(unmergedDictionary.location)).map((unmergedDictionary) => unmergedDictionary.filePath).filter(Boolean))].map((sourcePath) => (0, node_path.join)(configuration.system.baseDir, sourcePath)), configuration, void 0, { logError: false });
44
44
  const originalFillByPath = /* @__PURE__ */ new Map();
45
45
  for (const dictionary of sourceDictionaries) if (dictionary.filePath) originalFillByPath.set(dictionary.filePath, dictionary.fill);
46
46
  const affectedDictionaryKeys = /* @__PURE__ */ new Set();
@@ -78,7 +78,7 @@ const fill = async (options) => {
78
78
  const dictFill = originalFill !== void 0 ? originalFill : dictionaryOutput.fill;
79
79
  const hasDictionaryLevelFill = typeof dictFill === "string" || typeof dictFill === "function" || typeof dictFill === "object" && dictFill !== null;
80
80
  const isPerLocale = typeof dictionaryOutput.locale === "string";
81
- const effectiveFill = hasDictionaryLevelFill ? dictFill : isPerLocale ? configuration.dictionary?.fill ?? true : configuration.dictionary?.fill ?? false;
81
+ const effectiveFill = hasDictionaryLevelFill ? dictFill : isPerLocale ? configuration.dictionary?.fill ?? true : false;
82
82
  if (typeof effectiveFill === "string" || typeof effectiveFill === "function" || typeof effectiveFill === "object" && effectiveFill !== null) await require_fill_writeFill.writeFill({
83
83
  ...dictionaryOutput,
84
84
  fill: effectiveFill
@@ -1 +1 @@
1
- {"version":3,"file":"fill.cjs","names":["ensureArray","setupAI","x","getTargetUnmergedDictionaries","ANSIColors","listTranslationsTasks","translateDictionary","writeFill"],"sources":["../../../src/fill/fill.ts"],"sourcesContent":["import { basename, join, relative } from 'node:path';\n\nimport type { AIOptions } from '@intlayer/api';\nimport {\n loadContentDeclarations,\n prepareIntlayer,\n writeContentDeclaration,\n} from '@intlayer/chokidar/build';\nimport {\n type ListGitFilesOptions,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport {\n formatPath,\n getGlobalLimiter,\n getTaskLimiter,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizePath,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { Fill } from '@intlayer/types/dictionary';\nimport {\n ensureArray,\n type GetTargetDictionaryOptions,\n getTargetUnmergedDictionaries,\n} from '../getTargetDictionary';\nimport { setupAI } from '../utils/setupAI';\nimport {\n listTranslationsTasks,\n type TranslationTask,\n} from './listTranslationsTasks';\nimport { translateDictionary } from './translateDictionary';\nimport { writeFill } from './writeFill';\n\nconst NB_CONCURRENT_TRANSLATIONS = 7;\n\n// Arguments for the fill function\nexport type FillOptions = {\n sourceLocale?: Locale;\n outputLocales?: Locale | Locale[];\n mode?: 'complete' | 'review';\n gitOptions?: ListGitFilesOptions;\n aiOptions?: AIOptions; // Added aiOptions to be passed to translateJSON\n verbose?: boolean;\n nbConcurrentTranslations?: number;\n nbConcurrentTasks?: number; // NEW: number of tasks that may run at once\n build?: boolean;\n skipMetadata?: boolean;\n} & GetTargetDictionaryOptions;\n\n/**\n * Fill translations based on the provided options.\n */\nexport const fill = async (options?: FillOptions): Promise<void> => {\n const configuration = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n const appLogger = getAppLogger(configuration);\n\n if (options?.build === true) {\n await prepareIntlayer(configuration, { forceRun: true });\n } else if (typeof options?.build === 'undefined') {\n await prepareIntlayer(configuration);\n }\n\n const { defaultLocale, locales } = configuration.internationalization;\n const mode = options?.mode ?? 'complete';\n const baseLocale = options?.sourceLocale ?? defaultLocale;\n\n const outputLocales = options?.outputLocales\n ? ensureArray(options.outputLocales)\n : locales;\n\n const aiResult = await setupAI(configuration, options?.aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig, isCustomAI } = aiResult;\n\n if (isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n\n const targetUnmergedDictionaries =\n await getTargetUnmergedDictionaries(options);\n\n // Load the original source content declaration files to recover function-type\n // `fill` values that are lost when dictionaries are JSON-serialised into\n // unmerged_dictionaries.cjs. Dictionary-level fill takes priority over the\n // config-level fill, but we can only know that by reading the source files.\n const uniqueSourcePaths = [\n ...new Set(\n targetUnmergedDictionaries\n .filter(\n (unmergedDictionary) => unmergedDictionary.location !== 'remote'\n )\n .map((unmergedDictionary) => unmergedDictionary.filePath)\n .filter(Boolean) as string[]\n ),\n ];\n const sourceDictionaries = await loadContentDeclarations(\n uniqueSourcePaths.map((sourcePath) =>\n join(configuration.system.baseDir, sourcePath)\n ),\n configuration,\n undefined,\n {\n logError: false,\n }\n );\n // Map relative filePath → original fill value from the source file\n const originalFillByPath = new Map<string, Fill | undefined>();\n\n for (const dictionary of sourceDictionaries) {\n if (dictionary.filePath) {\n originalFillByPath.set(\n dictionary.filePath,\n dictionary.fill as Fill | undefined\n );\n }\n }\n\n const affectedDictionaryKeys = new Set<string>();\n\n targetUnmergedDictionaries.forEach((dict) => {\n affectedDictionaryKeys.add(dict.key);\n });\n\n const keysToProcess = Array.from(affectedDictionaryKeys);\n\n appLogger([\n 'Affected dictionary keys for processing:',\n keysToProcess.length > 0\n ? keysToProcess.map((key) => colorizeKey(key)).join(', ')\n : colorize('No keys found', ANSIColors.YELLOW),\n ]);\n\n if (keysToProcess.length === 0) return;\n\n /**\n * List the translations tasks\n *\n * Create a list of per-locale dictionaries to translate\n *\n * In 'complete' mode, filter only the missing locales to translate\n */\n const translationTasks: TranslationTask[] = listTranslationsTasks(\n targetUnmergedDictionaries.map((dictionary) => dictionary.localId!),\n outputLocales,\n mode,\n baseLocale,\n configuration\n );\n\n // AI calls in flight at once (translateJSON + metadata audit)\n const nbConcurrentTranslations =\n options?.nbConcurrentTranslations ?? NB_CONCURRENT_TRANSLATIONS;\n const globalLimiter = getGlobalLimiter(nbConcurrentTranslations);\n\n // NEW: number of *tasks* that may run at once (start/prepare/log/write)\n const nbConcurrentTasks = Math.max(\n 1,\n Math.min(\n options?.nbConcurrentTasks ?? nbConcurrentTranslations,\n translationTasks.length\n )\n );\n\n const taskLimiter = getTaskLimiter(nbConcurrentTasks);\n\n const runners = translationTasks.map((task) =>\n taskLimiter(async () => {\n const relativePath = relative(\n configuration?.system?.baseDir ?? process.cwd(),\n task?.dictionaryFilePath ?? ''\n );\n\n // log AFTER acquiring a task slot\n appLogger(\n `${task.dictionaryPreset} Processing ${colorizePath(basename(relativePath))}`,\n { level: 'info' }\n );\n\n const translationTaskResult = await translateDictionary(\n task,\n configuration,\n {\n mode,\n aiOptions: options?.aiOptions,\n fillMetadata: !options?.skipMetadata,\n onHandle: globalLimiter,\n aiClient,\n aiConfig,\n }\n );\n\n if (!translationTaskResult?.dictionaryOutput) return;\n\n const { dictionaryOutput, sourceLocale } = translationTaskResult;\n\n // Determine if we should write to separate files\n // - If dictionary has explicit fill setting (string, function, or object), use it\n // - If dictionary is per-locale AND has no explicit fill=false, use global fill config\n // - If dictionary is multilingual (no locale property), always write to same file\n //\n // NOTE: function-type fill values are lost during JSON serialisation of\n // unmerged_dictionaries.cjs. We recover them by checking the original\n // source file that was loaded above (originalFillByPath). Dictionary-level\n // fill always takes priority over config-level fill.\n const originalFill = originalFillByPath.get(\n dictionaryOutput.filePath ?? ''\n );\n\n // originalFill is undefined when the source file had no fill property; use\n // the (possibly JSON-preserved) dictionaryOutput.fill as a fallback so that\n // string/boolean fill values set directly on the dict still work.\n const dictFill: Fill | undefined =\n originalFill !== undefined ? originalFill : dictionaryOutput.fill;\n\n const hasDictionaryLevelFill =\n typeof dictFill === 'string' ||\n typeof dictFill === 'function' ||\n (typeof dictFill === 'object' && dictFill !== null);\n\n const isPerLocale = typeof dictionaryOutput.locale === 'string';\n\n const effectiveFill = hasDictionaryLevelFill\n ? dictFill\n : isPerLocale\n ? (configuration.dictionary?.fill ?? true)\n : (configuration.dictionary?.fill ?? false); // Multilingual dictionaries use config-level fill if set\n\n const isFillOtherFile =\n typeof effectiveFill === 'string' ||\n typeof effectiveFill === 'function' ||\n (typeof effectiveFill === 'object' && effectiveFill !== null);\n\n if (isFillOtherFile) {\n await writeFill(\n {\n ...dictionaryOutput,\n // Ensure fill is set on the dictionary for writeFill to use\n fill: effectiveFill,\n },\n outputLocales,\n [sourceLocale],\n configuration\n );\n } else {\n await writeContentDeclaration(dictionaryOutput, configuration);\n\n if (dictionaryOutput.filePath) {\n appLogger(\n `${task.dictionaryPreset} Content declaration written to ${formatPath(basename(dictionaryOutput.filePath))}`,\n { level: 'info' }\n );\n }\n }\n })\n );\n\n await Promise.all(runners);\n await (globalLimiter as any).onIdle();\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"fill.cjs","names":["ensureArray","setupAI","x","getTargetUnmergedDictionaries","ANSIColors","listTranslationsTasks","translateDictionary","writeFill"],"sources":["../../../src/fill/fill.ts"],"sourcesContent":["import { basename, join, relative } from 'node:path';\n\nimport type { AIOptions } from '@intlayer/api';\nimport {\n loadContentDeclarations,\n prepareIntlayer,\n writeContentDeclaration,\n} from '@intlayer/chokidar/build';\nimport {\n type ListGitFilesOptions,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport {\n formatPath,\n getGlobalLimiter,\n getTaskLimiter,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizePath,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { Fill } from '@intlayer/types/dictionary';\nimport {\n ensureArray,\n type GetTargetDictionaryOptions,\n getTargetUnmergedDictionaries,\n} from '../getTargetDictionary';\nimport { setupAI } from '../utils/setupAI';\nimport {\n listTranslationsTasks,\n type TranslationTask,\n} from './listTranslationsTasks';\nimport { translateDictionary } from './translateDictionary';\nimport { writeFill } from './writeFill';\n\nconst NB_CONCURRENT_TRANSLATIONS = 7;\n\n// Arguments for the fill function\nexport type FillOptions = {\n sourceLocale?: Locale;\n outputLocales?: Locale | Locale[];\n mode?: 'complete' | 'review';\n gitOptions?: ListGitFilesOptions;\n aiOptions?: AIOptions; // Added aiOptions to be passed to translateJSON\n verbose?: boolean;\n nbConcurrentTranslations?: number;\n nbConcurrentTasks?: number; // NEW: number of tasks that may run at once\n build?: boolean;\n skipMetadata?: boolean;\n} & GetTargetDictionaryOptions;\n\n/**\n * Fill translations based on the provided options.\n */\nexport const fill = async (options?: FillOptions): Promise<void> => {\n const configuration = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n const appLogger = getAppLogger(configuration);\n\n if (options?.build === true) {\n await prepareIntlayer(configuration, { forceRun: true });\n } else if (typeof options?.build === 'undefined') {\n await prepareIntlayer(configuration);\n }\n\n const { defaultLocale, locales } = configuration.internationalization;\n const mode = options?.mode ?? 'complete';\n const baseLocale = options?.sourceLocale ?? defaultLocale;\n\n const outputLocales = options?.outputLocales\n ? ensureArray(options.outputLocales)\n : locales;\n\n const aiResult = await setupAI(configuration, options?.aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig, isCustomAI } = aiResult;\n\n if (isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n\n const targetUnmergedDictionaries =\n await getTargetUnmergedDictionaries(options);\n\n // Load the original source content declaration files to recover function-type\n // `fill` values that are lost when dictionaries are JSON-serialised into\n // unmerged_dictionaries.cjs. Dictionary-level fill takes priority over the\n // config-level fill, but we can only know that by reading the source files.\n const uniqueSourcePaths = [\n ...new Set(\n targetUnmergedDictionaries\n .filter((unmergedDictionary) =>\n ['local', 'hybrid'].includes(unmergedDictionary.location!)\n )\n .map((unmergedDictionary) => unmergedDictionary.filePath)\n .filter(Boolean) as string[]\n ),\n ];\n const sourceDictionaries = await loadContentDeclarations(\n uniqueSourcePaths.map((sourcePath) =>\n join(configuration.system.baseDir, sourcePath)\n ),\n configuration,\n undefined,\n {\n logError: false,\n }\n );\n // Map relative filePath → original fill value from the source file\n const originalFillByPath = new Map<string, Fill | undefined>();\n\n for (const dictionary of sourceDictionaries) {\n if (dictionary.filePath) {\n originalFillByPath.set(\n dictionary.filePath,\n dictionary.fill as Fill | undefined\n );\n }\n }\n\n const affectedDictionaryKeys = new Set<string>();\n\n targetUnmergedDictionaries.forEach((dict) => {\n affectedDictionaryKeys.add(dict.key);\n });\n\n const keysToProcess = Array.from(affectedDictionaryKeys);\n\n appLogger([\n 'Affected dictionary keys for processing:',\n keysToProcess.length > 0\n ? keysToProcess.map((key) => colorizeKey(key)).join(', ')\n : colorize('No keys found', ANSIColors.YELLOW),\n ]);\n\n if (keysToProcess.length === 0) return;\n\n /**\n * List the translations tasks\n *\n * Create a list of per-locale dictionaries to translate\n *\n * In 'complete' mode, filter only the missing locales to translate\n */\n const translationTasks: TranslationTask[] = listTranslationsTasks(\n targetUnmergedDictionaries.map((dictionary) => dictionary.localId!),\n outputLocales,\n mode,\n baseLocale,\n configuration\n );\n\n // AI calls in flight at once (translateJSON + metadata audit)\n const nbConcurrentTranslations =\n options?.nbConcurrentTranslations ?? NB_CONCURRENT_TRANSLATIONS;\n const globalLimiter = getGlobalLimiter(nbConcurrentTranslations);\n\n // NEW: number of *tasks* that may run at once (start/prepare/log/write)\n const nbConcurrentTasks = Math.max(\n 1,\n Math.min(\n options?.nbConcurrentTasks ?? nbConcurrentTranslations,\n translationTasks.length\n )\n );\n\n const taskLimiter = getTaskLimiter(nbConcurrentTasks);\n\n const runners = translationTasks.map((task) =>\n taskLimiter(async () => {\n const relativePath = relative(\n configuration?.system?.baseDir ?? process.cwd(),\n task?.dictionaryFilePath ?? ''\n );\n\n // log AFTER acquiring a task slot\n appLogger(\n `${task.dictionaryPreset} Processing ${colorizePath(basename(relativePath))}`,\n { level: 'info' }\n );\n\n const translationTaskResult = await translateDictionary(\n task,\n configuration,\n {\n mode,\n aiOptions: options?.aiOptions,\n fillMetadata: !options?.skipMetadata,\n onHandle: globalLimiter,\n aiClient,\n aiConfig,\n }\n );\n\n if (!translationTaskResult?.dictionaryOutput) return;\n\n const { dictionaryOutput, sourceLocale } = translationTaskResult;\n\n // Determine if we should write to separate files\n // - If dictionary has explicit fill setting (string, function, or object), use it\n // - If dictionary is per-locale AND has no explicit fill=false, use global fill config\n // - If dictionary is multilingual (no locale property), always write to same file\n //\n // NOTE: function-type fill values are lost during JSON serialisation of\n // unmerged_dictionaries.cjs. We recover them by checking the original\n // source file that was loaded above (originalFillByPath). Dictionary-level\n // fill always takes priority over config-level fill.\n const originalFill = originalFillByPath.get(\n dictionaryOutput.filePath ?? ''\n );\n\n // originalFill is undefined when the source file had no fill property; use\n // the (possibly JSON-preserved) dictionaryOutput.fill as a fallback so that\n // string/boolean fill values set directly on the dict still work.\n const dictFill: Fill | undefined =\n originalFill !== undefined ? originalFill : dictionaryOutput.fill;\n\n const hasDictionaryLevelFill =\n typeof dictFill === 'string' ||\n typeof dictFill === 'function' ||\n (typeof dictFill === 'object' && dictFill !== null);\n\n const isPerLocale = typeof dictionaryOutput.locale === 'string';\n\n const effectiveFill = hasDictionaryLevelFill\n ? dictFill\n : isPerLocale\n ? (configuration.dictionary?.fill ?? true)\n : false; // Multilingual dictionaries (no locale property) always write to same file unless explicitly set on the dictionary\n\n const isFillOtherFile =\n typeof effectiveFill === 'string' ||\n typeof effectiveFill === 'function' ||\n (typeof effectiveFill === 'object' && effectiveFill !== null);\n\n if (isFillOtherFile) {\n await writeFill(\n {\n ...dictionaryOutput,\n // Ensure fill is set on the dictionary for writeFill to use\n fill: effectiveFill,\n },\n outputLocales,\n [sourceLocale],\n configuration\n );\n } else {\n await writeContentDeclaration(dictionaryOutput, configuration);\n\n if (dictionaryOutput.filePath) {\n appLogger(\n `${task.dictionaryPreset} Content declaration written to ${formatPath(basename(dictionaryOutput.filePath))}`,\n { level: 'info' }\n );\n }\n }\n })\n );\n\n await Promise.all(runners);\n await (globalLimiter as any).onIdle();\n};\n"],"mappings":"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"}
@@ -1,18 +1,29 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
3
3
  let node_path = require("node:path");
4
+ let _intlayer_chokidar_utils = require("@intlayer/chokidar/utils");
5
+ let _intlayer_config_colors = require("@intlayer/config/colors");
6
+ let _intlayer_config_logger = require("@intlayer/config/logger");
4
7
  let _intlayer_config_utils = require("@intlayer/config/utils");
5
8
 
6
9
  //#region src/fill/formatAutoFilledFilePath.ts
7
- const formatAutoFilledFilePath = async (autoFillField, dictionaryKey, dictionaryFilePath, baseDir, locale) => {
10
+ const formatAutoFilledFilePath = async (autoFillField, dictionaryKey, dictionaryFilePath, baseDir, configuration, locale) => {
8
11
  if (!autoFillField) throw new Error("autoFillField must be provided");
9
12
  if (!dictionaryKey || typeof dictionaryKey !== "string") throw new Error("dictionaryKey must be a non-empty string");
10
13
  if (!dictionaryFilePath || typeof dictionaryFilePath !== "string") throw new Error("dictionaryFilePath must be a non-empty string");
11
14
  if (!baseDir || typeof baseDir !== "string") throw new Error("baseDir must be a non-empty string");
15
+ const base = (0, node_path.basename)(dictionaryFilePath);
16
+ const { fileExtensions } = configuration.content;
17
+ const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));
18
+ const originalFileName = extensionMatch ? base.slice(0, -extensionMatch.length) : base.split(".")[0];
19
+ console.log({ extensionMatch });
20
+ if (!extensionMatch) throw new Error(`No extension found for file ${(0, _intlayer_config_logger.colorizePath)(dictionaryFilePath)}. Valid extensions are: ${(0, _intlayer_config_logger.colorize)(fileExtensions.join(", "), _intlayer_config_colors.GREY_DARK)}`);
12
21
  const result = await (0, _intlayer_config_utils.parseFilePathPattern)(autoFillField, {
13
22
  key: dictionaryKey,
14
- fileName: (0, node_path.basename)(dictionaryFilePath).split(".").slice(0, -2).join("."),
15
- locale
23
+ fileName: originalFileName,
24
+ locale,
25
+ extension: extensionMatch,
26
+ format: (0, _intlayer_chokidar_utils.getFormatFromExtension)(extensionMatch)
16
27
  });
17
28
  const absoluteDictionaryPath = (0, node_path.isAbsolute)(dictionaryFilePath) ? dictionaryFilePath : (0, node_path.resolve)(baseDir, dictionaryFilePath);
18
29
  if (result.startsWith("./") || result.startsWith("../")) return (0, node_path.resolve)((0, node_path.dirname)(absoluteDictionaryPath), result);
@@ -1 +1 @@
1
- {"version":3,"file":"formatAutoFilledFilePath.cjs","names":[],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"sourcesContent":["import { basename, dirname, isAbsolute, normalize, resolve } from 'node:path';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\n\nexport const formatAutoFilledFilePath = async (\n autoFillField: FilePathPattern,\n dictionaryKey: string,\n dictionaryFilePath: string,\n baseDir: string,\n locale?: Locale\n): Promise<string> => {\n // Validate inputs\n if (!autoFillField) {\n throw new Error('autoFillField must be provided');\n }\n if (!dictionaryKey || typeof dictionaryKey !== 'string') {\n throw new Error('dictionaryKey must be a non-empty string');\n }\n if (!dictionaryFilePath || typeof dictionaryFilePath !== 'string') {\n throw new Error('dictionaryFilePath must be a non-empty string');\n }\n if (!baseDir || typeof baseDir !== 'string') {\n throw new Error('baseDir must be a non-empty string');\n }\n\n // Extract the original filename without extensions (.content.ts -> dictionaryFieldEditor)\n const originalFileName = basename(dictionaryFilePath)\n .split('.')\n .slice(0, -2) // Remove last 2 extensions (.content.tsx)\n .join('.');\n\n // Replace placeholders in autoFillField\n const result: string = await parseFilePathPattern(autoFillField, {\n key: dictionaryKey,\n fileName: originalFileName,\n locale,\n });\n\n // Normalize the dictionary file path - if it's relative, make it absolute relative to baseDir\n const absoluteDictionaryPath = isAbsolute(dictionaryFilePath)\n ? dictionaryFilePath\n : resolve(baseDir, dictionaryFilePath);\n\n // Handle relative paths (starting with ./ or ../)\n if (result.startsWith('./') || result.startsWith('../')) {\n const fileDir = dirname(absoluteDictionaryPath);\n const resolvedPath = resolve(fileDir, result);\n\n return resolvedPath;\n }\n\n // Handle absolute paths\n if (isAbsolute(result)) {\n const normalizedResult = normalize(result);\n const normalizedBaseDir = normalize(baseDir);\n\n // Check if it's relative to baseDir (starts with /)\n // but not a system path (like /usr/local)\n if (\n result.startsWith('/') &&\n !normalizedResult.startsWith(normalizedBaseDir)\n ) {\n // Try to resolve it relative to baseDir first\n const relativeToBase = resolve(baseDir, result.substring(1));\n\n // If the path doesn't exist in common system directories, treat as relative to baseDir\n if (\n !result.startsWith('/usr/') &&\n !result.startsWith('/etc/') &&\n !result.startsWith('/var/') &&\n !result.startsWith('/home/') &&\n !result.startsWith('/Users/')\n ) {\n return relativeToBase;\n }\n }\n\n // It's a true system absolute path\n return normalizedResult;\n }\n\n // Default case: treat as relative to baseDir\n return normalize(result);\n};\n"],"mappings":";;;;;;AAKA,MAAa,2BAA2B,OACtC,eACA,eACA,oBACA,SACA,WACoB;AAEpB,KAAI,CAAC,cACH,OAAM,IAAI,MAAM,iCAAiC;AAEnD,KAAI,CAAC,iBAAiB,OAAO,kBAAkB,SAC7C,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,CAAC,sBAAsB,OAAO,uBAAuB,SACvD,OAAM,IAAI,MAAM,gDAAgD;AAElE,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,qCAAqC;CAUvD,MAAM,SAAiB,uDAA2B,eAAe;EAC/D,KAAK;EACL,kCARgC,mBAAmB,CAClD,MAAM,IAAI,CACV,MAAM,GAAG,GAAG,CACZ,KAAK,IAKoB;EAC1B;EACD,CAAC;CAGF,MAAM,mDAAoC,mBAAmB,GACzD,4CACQ,SAAS,mBAAmB;AAGxC,KAAI,OAAO,WAAW,KAAK,IAAI,OAAO,WAAW,MAAM,CAIrD,sDAHwB,uBACY,EAAE,OAEnB;AAIrB,+BAAe,OAAO,EAAE;EACtB,MAAM,4CAA6B,OAAO;EAC1C,MAAM,6CAA8B,QAAQ;AAI5C,MACE,OAAO,WAAW,IAAI,IACtB,CAAC,iBAAiB,WAAW,kBAAkB,EAC/C;GAEA,MAAM,wCAAyB,SAAS,OAAO,UAAU,EAAE,CAAC;AAG5D,OACE,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,SAAS,IAC5B,CAAC,OAAO,WAAW,UAAU,CAE7B,QAAO;;AAKX,SAAO;;AAIT,iCAAiB,OAAO"}
1
+ {"version":3,"file":"formatAutoFilledFilePath.cjs","names":["GREY_DARK"],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"sourcesContent":["import { basename, dirname, isAbsolute, normalize, resolve } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport { GREY_DARK } from '@intlayer/config/colors';\nimport { colorize, colorizePath } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\n\nexport const formatAutoFilledFilePath = async (\n autoFillField: FilePathPattern,\n dictionaryKey: string,\n dictionaryFilePath: string,\n baseDir: string,\n configuration: IntlayerConfig,\n locale?: Locale\n): Promise<string> => {\n // Validate inputs\n if (!autoFillField) {\n throw new Error('autoFillField must be provided');\n }\n if (!dictionaryKey || typeof dictionaryKey !== 'string') {\n throw new Error('dictionaryKey must be a non-empty string');\n }\n if (!dictionaryFilePath || typeof dictionaryFilePath !== 'string') {\n throw new Error('dictionaryFilePath must be a non-empty string');\n }\n if (!baseDir || typeof baseDir !== 'string') {\n throw new Error('baseDir must be a non-empty string');\n }\n\n // Extract the original filename accurately\n const base = basename(dictionaryFilePath);\n\n const { fileExtensions } = configuration.content;\n const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));\n\n const originalFileName = extensionMatch\n ? base.slice(0, -extensionMatch.length)\n : base.split('.')[0];\n\n console.log({ extensionMatch });\n\n if (!extensionMatch) {\n throw new Error(\n `No extension found for file ${colorizePath(dictionaryFilePath)}. Valid extensions are: ${colorize(\n fileExtensions.join(', '),\n GREY_DARK\n )}`\n );\n }\n\n // Replace placeholders in autoFillField\n const result: string = await parseFilePathPattern(autoFillField, {\n key: dictionaryKey,\n fileName: originalFileName,\n locale,\n extension: extensionMatch,\n format: getFormatFromExtension(extensionMatch),\n });\n\n // Normalize the dictionary file path - if it's relative, make it absolute relative to baseDir\n const absoluteDictionaryPath = isAbsolute(dictionaryFilePath)\n ? dictionaryFilePath\n : resolve(baseDir, dictionaryFilePath);\n\n // Handle relative paths (starting with ./ or ../)\n if (result.startsWith('./') || result.startsWith('../')) {\n const fileDir = dirname(absoluteDictionaryPath);\n const resolvedPath = resolve(fileDir, result);\n\n return resolvedPath;\n }\n\n // Handle absolute paths\n if (isAbsolute(result)) {\n const normalizedResult = normalize(result);\n const normalizedBaseDir = normalize(baseDir);\n\n // Check if it's relative to baseDir (starts with /)\n // but not a system path (like /usr/local)\n if (\n result.startsWith('/') &&\n !normalizedResult.startsWith(normalizedBaseDir)\n ) {\n // Try to resolve it relative to baseDir first\n const relativeToBase = resolve(baseDir, result.substring(1));\n\n // If the path doesn't exist in common system directories, treat as relative to baseDir\n if (\n !result.startsWith('/usr/') &&\n !result.startsWith('/etc/') &&\n !result.startsWith('/var/') &&\n !result.startsWith('/home/') &&\n !result.startsWith('/Users/')\n ) {\n return relativeToBase;\n }\n }\n\n // It's a true system absolute path\n return normalizedResult;\n }\n\n // Default case: treat as relative to baseDir\n return normalize(result);\n};\n"],"mappings":";;;;;;;;;AASA,MAAa,2BAA2B,OACtC,eACA,eACA,oBACA,SACA,eACA,WACoB;AAEpB,KAAI,CAAC,cACH,OAAM,IAAI,MAAM,iCAAiC;AAEnD,KAAI,CAAC,iBAAiB,OAAO,kBAAkB,SAC7C,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,CAAC,sBAAsB,OAAO,uBAAuB,SACvD,OAAM,IAAI,MAAM,gDAAgD;AAElE,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,qCAAqC;CAIvD,MAAM,+BAAgB,mBAAmB;CAEzC,MAAM,EAAE,mBAAmB,cAAc;CACzC,MAAM,iBAAiB,eAAe,MAAM,QAAQ,KAAK,SAAS,IAAI,CAAC;CAEvE,MAAM,mBAAmB,iBACrB,KAAK,MAAM,GAAG,CAAC,eAAe,OAAO,GACrC,KAAK,MAAM,IAAI,CAAC;AAEpB,SAAQ,IAAI,EAAE,gBAAgB,CAAC;AAE/B,KAAI,CAAC,eACH,OAAM,IAAI,MACR,yEAA4C,mBAAmB,CAAC,gEAC9D,eAAe,KAAK,KAAK,EACzBA,kCACD,GACF;CAIH,MAAM,SAAiB,uDAA2B,eAAe;EAC/D,KAAK;EACL,UAAU;EACV;EACA,WAAW;EACX,6DAA+B,eAAe;EAC/C,CAAC;CAGF,MAAM,mDAAoC,mBAAmB,GACzD,4CACQ,SAAS,mBAAmB;AAGxC,KAAI,OAAO,WAAW,KAAK,IAAI,OAAO,WAAW,MAAM,CAIrD,sDAHwB,uBACY,EAAE,OAEnB;AAIrB,+BAAe,OAAO,EAAE;EACtB,MAAM,4CAA6B,OAAO;EAC1C,MAAM,6CAA8B,QAAQ;AAI5C,MACE,OAAO,WAAW,IAAI,IACtB,CAAC,iBAAiB,WAAW,kBAAkB,EAC/C;GAEA,MAAM,wCAAyB,SAAS,OAAO,UAAU,EAAE,CAAC;AAG5D,OACE,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,SAAS,IAC5B,CAAC,OAAO,WAAW,UAAU,CAE7B,QAAO;;AAKX,SAAO;;AAIT,iCAAiB,OAAO"}
@@ -17,10 +17,14 @@ const groupByFilePath = (entries) => entries.reduce((acc, curr) => {
17
17
  const formatFillData = async (fillField, localeList, filePath, dictionaryKey, configuration) => {
18
18
  if (!fillField || typeof fillField === "boolean") return [];
19
19
  const { baseDir } = configuration.system;
20
- const { defaultLocale } = configuration.internationalization;
20
+ const { defaultLocale, locales } = configuration.internationalization;
21
21
  const extension = (0, node_path.extname)(filePath);
22
22
  const base = (0, node_path.basename)(filePath);
23
- const cleanComponentFileName = base.includes(".content.") ? base.split(".content.")[0] : base.split(".")[0];
23
+ const { fileExtensions } = configuration.content;
24
+ const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));
25
+ let cleanComponentFileName = extensionMatch ? base.slice(0, -extensionMatch.length) : base.split(".")[0];
26
+ const sourceLocaleMatch = locales.find((loc) => cleanComponentFileName.endsWith(`.${loc}`));
27
+ if (sourceLocaleMatch) cleanComponentFileName = cleanComponentFileName.slice(0, -(sourceLocaleMatch.length + 1));
24
28
  const uncapitalizedName = cleanComponentFileName.charAt(0).toLowerCase() + cleanComponentFileName.slice(1);
25
29
  const componentFormat = (0, _intlayer_chokidar_utils.getFormatFromExtension)(extension);
26
30
  const getContext = (locale, patternString) => {
@@ -38,7 +42,7 @@ const formatFillData = async (fillField, localeList, filePath, dictionaryKey, co
38
42
  componentExtension: extension,
39
43
  format,
40
44
  locale,
41
- extension: configuration.content.fileExtensions[0]
45
+ extension: configuration.content.fileExtensions.find((ext) => ext.endsWith(extension)) ?? configuration.content.fileExtensions[0]
42
46
  };
43
47
  };
44
48
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"formatFillData.cjs","names":["resolveOutputPattern","getPatternForLocale"],"sources":["../../../src/fill/formatFillData.ts"],"sourcesContent":["import { basename, dirname, extname, relative } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DictionaryKey, Fill } from '@intlayer/types/dictionary';\nimport type { FilePathPatternContext } from '@intlayer/types/filePathPattern';\nimport {\n getPatternForLocale,\n resolveOutputPattern,\n} from '../utils/getOutputFilePath';\n\nexport type FillData = {\n localeList: Locale[];\n filePath: string;\n isPerLocale: boolean;\n};\n\n/** Merge FillData entries that resolve to the same file path. */\nconst groupByFilePath = (entries: FillData[]): FillData[] =>\n entries.reduce((acc, curr) => {\n const existing = acc.find((item) => item.filePath === curr.filePath);\n if (existing) {\n for (const loc of curr.localeList) {\n if (!existing.localeList.includes(loc)) existing.localeList.push(loc);\n }\n // Multiple locales sharing one path → multilingual file\n existing.isPerLocale = false;\n } else {\n acc.push(curr);\n }\n return acc;\n }, [] as FillData[]);\n\nexport const formatFillData = async (\n fillField: Fill,\n localeList: Locale[],\n filePath: string,\n dictionaryKey: DictionaryKey,\n configuration: IntlayerConfig\n): Promise<FillData[]> => {\n if (!fillField || typeof fillField === 'boolean') return [];\n\n const { baseDir } = configuration.system;\n const { defaultLocale } = configuration.internationalization;\n\n const extension = extname(filePath);\n const base = basename(filePath);\n const cleanComponentFileName = base.includes('.content.')\n ? base.split('.content.')[0]\n : base.split('.')[0];\n const uncapitalizedName =\n cleanComponentFileName.charAt(0).toLowerCase() +\n cleanComponentFileName.slice(1);\n const componentFormat = getFormatFromExtension(extension);\n\n const getContext = (\n locale: Locale,\n patternString?: string\n ): FilePathPatternContext => {\n let format: FilePathPatternContext['format'] = 'json';\n if (patternString) {\n const extFormat = getFormatFromExtension(extname(patternString) as any);\n if (extFormat) format = extFormat as any;\n }\n return {\n key: dictionaryKey,\n componentDirPath: relative(baseDir, dirname(filePath)),\n componentFileName: cleanComponentFileName,\n fileName: uncapitalizedName,\n componentFormat:\n componentFormat as FilePathPatternContext['componentFormat'],\n componentExtension:\n extension as FilePathPatternContext['componentExtension'],\n format,\n locale,\n extension: configuration.content.fileExtensions[0],\n };\n };\n\n /**\n * Evaluate a scalar Fill pattern (string or function) for a list of locales.\n *\n * - Uses a dummy locale probe to detect whether the pattern varies per locale.\n * - `forcePerLocale` overrides the probe for object-fill entries where each\n * locale always maps to its own dedicated file regardless of whether the\n * path itself contains the locale string.\n */\n const processPattern = async (\n pattern: Fill,\n locales: Locale[],\n forcePerLocale = false\n ): Promise<FillData[]> => {\n const dummyLocale = '###########locale###########' as Locale;\n const patternString = typeof pattern === 'string' ? pattern : undefined;\n\n const dummyPath = await resolveOutputPattern(\n pattern,\n dummyLocale,\n getContext(dummyLocale, patternString),\n filePath,\n baseDir\n );\n const isPatternPerLocale =\n forcePerLocale ||\n (typeof dummyPath === 'string' && dummyPath.includes(dummyLocale));\n\n if (isPatternPerLocale) {\n const resolvedPaths: FillData[] = [];\n for (const locale of locales) {\n const absolutePath = await resolveOutputPattern(\n pattern,\n locale,\n getContext(locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath !== false) {\n resolvedPaths.push({\n filePath: absolutePath,\n localeList: [locale],\n isPerLocale: true,\n });\n }\n }\n return groupByFilePath(resolvedPaths);\n }\n\n // Single multilingual path — resolve using the default locale for context\n const absolutePath = await resolveOutputPattern(\n pattern,\n defaultLocale as Locale,\n getContext(defaultLocale as Locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath === false) return [];\n return [\n { filePath: absolutePath, localeList: locales, isPerLocale: false },\n ];\n };\n\n // Object fill: each locale key maps to its own pattern.\n // Object fill entries are always per-locale in intent (each locale → its own file).\n if (typeof fillField === 'object' && fillField !== null) {\n const results: FillData[] = [];\n for (const locale of localeList) {\n const pattern = getPatternForLocale(fillField, locale);\n if (pattern) {\n const res = await processPattern(pattern, [locale], true);\n results.push(...res);\n }\n }\n return groupByFilePath(results);\n }\n\n // Scalar string or function pattern\n return processPattern(fillField, localeList);\n};\n"],"mappings":";;;;;;;;AAkBA,MAAM,mBAAmB,YACvB,QAAQ,QAAQ,KAAK,SAAS;CAC5B,MAAM,WAAW,IAAI,MAAM,SAAS,KAAK,aAAa,KAAK,SAAS;AACpE,KAAI,UAAU;AACZ,OAAK,MAAM,OAAO,KAAK,WACrB,KAAI,CAAC,SAAS,WAAW,SAAS,IAAI,CAAE,UAAS,WAAW,KAAK,IAAI;AAGvE,WAAS,cAAc;OAEvB,KAAI,KAAK,KAAK;AAEhB,QAAO;GACN,EAAE,CAAe;AAEtB,MAAa,iBAAiB,OAC5B,WACA,YACA,UACA,eACA,kBACwB;AACxB,KAAI,CAAC,aAAa,OAAO,cAAc,UAAW,QAAO,EAAE;CAE3D,MAAM,EAAE,YAAY,cAAc;CAClC,MAAM,EAAE,kBAAkB,cAAc;CAExC,MAAM,mCAAoB,SAAS;CACnC,MAAM,+BAAgB,SAAS;CAC/B,MAAM,yBAAyB,KAAK,SAAS,YAAY,GACrD,KAAK,MAAM,YAAY,CAAC,KACxB,KAAK,MAAM,IAAI,CAAC;CACpB,MAAM,oBACJ,uBAAuB,OAAO,EAAE,CAAC,aAAa,GAC9C,uBAAuB,MAAM,EAAE;CACjC,MAAM,uEAAyC,UAAU;CAEzD,MAAM,cACJ,QACA,kBAC2B;EAC3B,IAAI,SAA2C;AAC/C,MAAI,eAAe;GACjB,MAAM,wFAA2C,cAAc,CAAQ;AACvE,OAAI,UAAW,UAAS;;AAE1B,SAAO;GACL,KAAK;GACL,0CAA2B,gCAAiB,SAAS,CAAC;GACtD,mBAAmB;GACnB,UAAU;GAER;GACF,oBACE;GACF;GACA;GACA,WAAW,cAAc,QAAQ,eAAe;GACjD;;;;;;;;;;CAWH,MAAM,iBAAiB,OACrB,SACA,SACA,iBAAiB,UACO;EACxB,MAAM,cAAc;EACpB,MAAM,gBAAgB,OAAO,YAAY,WAAW,UAAU;EAE9D,MAAM,YAAY,MAAMA,qDACtB,SACA,aACA,WAAW,aAAa,cAAc,EACtC,UACA,QACD;AAKD,MAHE,kBACC,OAAO,cAAc,YAAY,UAAU,SAAS,YAAY,EAE3C;GACtB,MAAM,gBAA4B,EAAE;AACpC,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,eAAe,MAAMA,qDACzB,SACA,QACA,WAAW,QAAQ,cAAc,EACjC,UACA,QACD;AACD,QAAI,iBAAiB,MACnB,eAAc,KAAK;KACjB,UAAU;KACV,YAAY,CAAC,OAAO;KACpB,aAAa;KACd,CAAC;;AAGN,UAAO,gBAAgB,cAAc;;EAIvC,MAAM,eAAe,MAAMA,qDACzB,SACA,eACA,WAAW,eAAyB,cAAc,EAClD,UACA,QACD;AACD,MAAI,iBAAiB,MAAO,QAAO,EAAE;AACrC,SAAO,CACL;GAAE,UAAU;GAAc,YAAY;GAAS,aAAa;GAAO,CACpE;;AAKH,KAAI,OAAO,cAAc,YAAY,cAAc,MAAM;EACvD,MAAM,UAAsB,EAAE;AAC9B,OAAK,MAAM,UAAU,YAAY;GAC/B,MAAM,UAAUC,oDAAoB,WAAW,OAAO;AACtD,OAAI,SAAS;IACX,MAAM,MAAM,MAAM,eAAe,SAAS,CAAC,OAAO,EAAE,KAAK;AACzD,YAAQ,KAAK,GAAG,IAAI;;;AAGxB,SAAO,gBAAgB,QAAQ;;AAIjC,QAAO,eAAe,WAAW,WAAW"}
1
+ {"version":3,"file":"formatFillData.cjs","names":["resolveOutputPattern","getPatternForLocale"],"sources":["../../../src/fill/formatFillData.ts"],"sourcesContent":["import { basename, dirname, extname, relative } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DictionaryKey, Fill } from '@intlayer/types/dictionary';\nimport type { FilePathPatternContext } from '@intlayer/types/filePathPattern';\nimport {\n getPatternForLocale,\n resolveOutputPattern,\n} from '../utils/getOutputFilePath';\n\nexport type FillData = {\n localeList: Locale[];\n filePath: string;\n isPerLocale: boolean;\n};\n\n/** Merge FillData entries that resolve to the same file path. */\nconst groupByFilePath = (entries: FillData[]): FillData[] =>\n entries.reduce((acc, curr) => {\n const existing = acc.find((item) => item.filePath === curr.filePath);\n if (existing) {\n for (const loc of curr.localeList) {\n if (!existing.localeList.includes(loc)) existing.localeList.push(loc);\n }\n // Multiple locales sharing one path → multilingual file\n existing.isPerLocale = false;\n } else {\n acc.push(curr);\n }\n return acc;\n }, [] as FillData[]);\n\nexport const formatFillData = async (\n fillField: Fill,\n localeList: Locale[],\n filePath: string,\n dictionaryKey: DictionaryKey,\n configuration: IntlayerConfig\n): Promise<FillData[]> => {\n if (!fillField || typeof fillField === 'boolean') return [];\n\n const { baseDir } = configuration.system;\n const { defaultLocale, locales } = configuration.internationalization;\n\n const extension = extname(filePath);\n const base = basename(filePath);\n\n const { fileExtensions } = configuration.content;\n const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));\n\n let cleanComponentFileName = extensionMatch\n ? base.slice(0, -extensionMatch.length)\n : base.split('.')[0];\n\n // Strip source locale if present\n const sourceLocaleMatch = locales.find((loc) =>\n cleanComponentFileName.endsWith(`.${loc}`)\n );\n if (sourceLocaleMatch) {\n cleanComponentFileName = cleanComponentFileName.slice(\n 0,\n -(sourceLocaleMatch.length + 1)\n );\n }\n\n const uncapitalizedName =\n cleanComponentFileName.charAt(0).toLowerCase() +\n cleanComponentFileName.slice(1);\n const componentFormat = getFormatFromExtension(extension);\n\n const getContext = (\n locale: Locale,\n patternString?: string\n ): FilePathPatternContext => {\n let format: FilePathPatternContext['format'] = 'json';\n if (patternString) {\n const extFormat = getFormatFromExtension(extname(patternString) as any);\n if (extFormat) format = extFormat as any;\n }\n return {\n key: dictionaryKey,\n componentDirPath: relative(baseDir, dirname(filePath)),\n componentFileName: cleanComponentFileName,\n fileName: uncapitalizedName,\n componentFormat:\n componentFormat as FilePathPatternContext['componentFormat'],\n componentExtension:\n extension as FilePathPatternContext['componentExtension'],\n format,\n locale,\n extension:\n configuration.content.fileExtensions.find((ext) =>\n ext.endsWith(extension)\n ) ?? configuration.content.fileExtensions[0],\n };\n };\n\n /**\n * Evaluate a scalar Fill pattern (string or function) for a list of locales.\n *\n * - Uses a dummy locale probe to detect whether the pattern varies per locale.\n * - `forcePerLocale` overrides the probe for object-fill entries where each\n * locale always maps to its own dedicated file regardless of whether the\n * path itself contains the locale string.\n */\n const processPattern = async (\n pattern: Fill,\n locales: Locale[],\n forcePerLocale = false\n ): Promise<FillData[]> => {\n const dummyLocale = '###########locale###########' as Locale;\n const patternString = typeof pattern === 'string' ? pattern : undefined;\n\n const dummyPath = await resolveOutputPattern(\n pattern,\n dummyLocale,\n getContext(dummyLocale, patternString),\n filePath,\n baseDir\n );\n const isPatternPerLocale =\n forcePerLocale ||\n (typeof dummyPath === 'string' && dummyPath.includes(dummyLocale));\n\n if (isPatternPerLocale) {\n const resolvedPaths: FillData[] = [];\n for (const locale of locales) {\n const absolutePath = await resolveOutputPattern(\n pattern,\n locale,\n getContext(locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath !== false) {\n resolvedPaths.push({\n filePath: absolutePath,\n localeList: [locale],\n isPerLocale: true,\n });\n }\n }\n return groupByFilePath(resolvedPaths);\n }\n\n // Single multilingual path — resolve using the default locale for context\n const absolutePath = await resolveOutputPattern(\n pattern,\n defaultLocale as Locale,\n getContext(defaultLocale as Locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath === false) return [];\n return [\n { filePath: absolutePath, localeList: locales, isPerLocale: false },\n ];\n };\n\n // Object fill: each locale key maps to its own pattern.\n // Object fill entries are always per-locale in intent (each locale → its own file).\n if (typeof fillField === 'object' && fillField !== null) {\n const results: FillData[] = [];\n for (const locale of localeList) {\n const pattern = getPatternForLocale(fillField, locale);\n if (pattern) {\n const res = await processPattern(pattern, [locale], true);\n results.push(...res);\n }\n }\n return groupByFilePath(results);\n }\n\n // Scalar string or function pattern\n return processPattern(fillField, localeList);\n};\n"],"mappings":"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"}
@@ -32,7 +32,7 @@ const serializeError = (error) => {
32
32
  return String(error);
33
33
  }
34
34
  };
35
- const CHUNK_SIZE = 1500;
35
+ const CHUNK_SIZE = 4e3;
36
36
  const GROUP_MAX_RETRY = 2;
37
37
  const MAX_RETRY = 3;
38
38
  const RETRY_DELAY = 1e3 * 10;
@@ -89,11 +89,13 @@ const translateDictionary = async (task, configuration, options) => {
89
89
  let dictionaryToProcess = structuredClone(baseUnmergedDictionary);
90
90
  let targetLocaleDictionary;
91
91
  if (typeof baseUnmergedDictionary.locale === "string") {
92
- const targetLocaleFilePath = baseUnmergedDictionary.filePath?.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`);
93
- targetLocaleDictionary = (targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0) ?? {
92
+ const targetUnmergedDictionary = unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.locale === targetLocale && dict.filePath !== baseUnmergedDictionary.filePath);
93
+ const sourceFilePath = baseUnmergedDictionary.filePath ?? "";
94
+ const derivedTargetFilePath = sourceFilePath.includes(`/${task.sourceLocale}/`) ? sourceFilePath.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`) : sourceFilePath.replace(new RegExp(`(^|/)${task.sourceLocale}(\\.[^/]+)$`), `$1${targetLocale}$2`);
95
+ targetLocaleDictionary = targetUnmergedDictionary ?? {
94
96
  key: baseUnmergedDictionary.key,
95
97
  content: {},
96
- filePath: targetLocaleFilePath,
98
+ filePath: derivedTargetFilePath || void 0,
97
99
  locale: targetLocale
98
100
  };
99
101
  } else {
@@ -1 +1 @@
1
- {"version":3,"file":"translateDictionary.cjs","names":["ANSIColors","extractTranslatableContent","reinsertTranslatedContent","deepMergeContent"],"sources":["../../../src/fill/translateDictionary.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport type { AIConfig } from '@intlayer/ai';\nimport { type AIOptions, getIntlayerAPIProxy } from '@intlayer/api';\nimport {\n chunkJSON,\n excludeObjectFormat,\n formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n verifyIdenticObjectFormat,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n colorizePath,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { retryManager } from '@intlayer/config/utils';\nimport {\n getFilterMissingTranslationsDictionary,\n getMultilingualDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport type { AIClient } from '../utils/setupAI';\nimport { deepMergeContent } from './deepMergeContent';\nimport {\n extractTranslatableContent,\n reinsertTranslatedContent,\n} from './extractTranslatableContent';\nimport type { TranslationTask } from './listTranslationsTasks';\n\ntype TranslateDictionaryResult = TranslationTask & {\n dictionaryOutput: Dictionary | null;\n};\n\ntype TranslateDictionaryOptions = {\n mode: 'complete' | 'review';\n aiOptions?: AIOptions;\n fillMetadata?: boolean;\n onHandle?: ReturnType<\n typeof import('@intlayer/chokidar/utils').getGlobalLimiter\n >;\n onSuccess?: () => void;\n onError?: (error: unknown) => void;\n getAbortError?: () => Error | null;\n aiClient?: AIClient;\n aiConfig?: AIConfig;\n};\n\nconst createChunkPreset = (chunkIndex: number, totalChunks: number) => {\n if (totalChunks <= 1) return '';\n return colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n colorizeNumber(chunkIndex + 1),\n colorize(`/${totalChunks}`, ANSIColors.GREY_DARK),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 5 }\n );\n};\n\nconst hasMissingMetadata = (dictionary: Dictionary) =>\n !dictionary.description || !dictionary.title || !dictionary.tags;\n\nconst serializeError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.cause\n ? `${error.message} (cause: ${String(error.cause)})`\n : error.message;\n }\n if (typeof error === 'string') return error;\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n};\n\nconst CHUNK_SIZE = 1500; // Smaller chunks for better accuracy and structural integrity\nconst GROUP_MAX_RETRY = 2;\nconst MAX_RETRY = 3;\nconst RETRY_DELAY = 1000 * 10; // 10 seconds\n\nconst MAX_FOLLOWING_ERRORS = 10; // 10 errors in a row, hard exit the process\nlet followingErrors = 0;\n\nexport const translateDictionary = async (\n task: TranslationTask,\n configuration: IntlayerConfig,\n options?: TranslateDictionaryOptions\n): Promise<TranslateDictionaryResult> => {\n const appLogger = getAppLogger(configuration);\n const intlayerAPI = getIntlayerAPIProxy(undefined, configuration);\n\n const { mode, aiOptions, fillMetadata, aiClient, aiConfig } = {\n mode: 'complete',\n fillMetadata: true,\n ...options,\n } as const;\n\n const notifySuccess = () => {\n followingErrors = 0;\n options?.onSuccess?.();\n };\n\n const result = await retryManager(\n async () => {\n const unmergedDictionariesRecord = getUnmergedDictionaries(configuration);\n\n const baseUnmergedDictionary: Dictionary | undefined =\n unmergedDictionariesRecord[task.dictionaryKey].find(\n (dict) => dict.localId === task.dictionaryLocalId\n );\n\n if (!baseUnmergedDictionary) {\n appLogger(\n `${task.dictionaryPreset}Dictionary not found in unmergedDictionariesRecord. Skipping.`,\n {\n level: 'warn',\n }\n );\n return { ...task, dictionaryOutput: null };\n }\n\n let metadata:\n | Pick<Dictionary, 'description' | 'title' | 'tags'>\n | undefined;\n\n if (\n fillMetadata &&\n (hasMissingMetadata(baseUnmergedDictionary) || mode === 'review')\n ) {\n const defaultLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n configuration.internationalization.defaultLocale\n );\n\n appLogger(\n `${task.dictionaryPreset} Filling missing metadata for ${colorizePath(basename(baseUnmergedDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const runAudit = async () => {\n if (aiClient && aiConfig) {\n const result = await aiClient.auditDictionaryMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiConfig,\n });\n\n return {\n data: result,\n };\n }\n\n return await intlayerAPI.ai.auditContentDeclarationMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiOptions,\n });\n };\n\n const metadataResult = options?.onHandle\n ? await options.onHandle(runAudit)\n : await runAudit();\n\n metadata = metadataResult.data?.fileContent;\n }\n\n const translatedContentResults = await Promise.all(\n task.targetLocales.map(async (targetLocale) => {\n /**\n * In complete mode, for large dictionaries, we want to filter all content that is already translated\n *\n * targetLocale: fr\n *\n * { test1: t({ ar: 'Hello', en: 'Hello', fr: 'Bonjour' } }) -> {}\n * { test2: t({ ar: 'Hello', en: 'Hello' }) } -> { test2: t({ ar: 'Hello', en: 'Hello' }) }\n *\n */\n // Reset to base dictionary for each locale to ensure we filter from the original\n let dictionaryToProcess = structuredClone(baseUnmergedDictionary);\n\n let targetLocaleDictionary: Dictionary;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // For per-locale files, the content is already in simple JSON format (not translation nodes)\n // The base dictionary is already the source locale content\n\n // Load the existing target locale dictionary\n const targetLocaleFilePath =\n baseUnmergedDictionary.filePath?.replace(\n new RegExp(`/${task.sourceLocale}/`, 'g'),\n `/${targetLocale}/`\n );\n\n // Find the target locale dictionary in unmerged dictionaries\n const targetUnmergedDictionary = targetLocaleFilePath\n ? unmergedDictionariesRecord[task.dictionaryKey]?.find(\n (dict) =>\n dict.filePath === targetLocaleFilePath &&\n dict.locale === targetLocale\n )\n : undefined;\n\n targetLocaleDictionary = targetUnmergedDictionary ?? {\n key: baseUnmergedDictionary.key,\n content: {},\n filePath: targetLocaleFilePath,\n locale: targetLocale,\n };\n } else {\n // For multilingual dictionaries\n if (mode === 'complete') {\n // Remove all nodes that don't have any content to translate\n dictionaryToProcess = getFilterMissingTranslationsDictionary(\n dictionaryToProcess,\n targetLocale\n );\n }\n\n dictionaryToProcess = getPerLocaleDictionary(\n dictionaryToProcess,\n task.sourceLocale\n );\n\n targetLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n targetLocale\n );\n }\n\n // Filter to only untranslated fields, preserving explicit null values as\n // default-locale fallback markers. Applied after both paths converge so\n // the same logic covers per-locale and multilingual dictionaries.\n if (mode === 'complete') {\n dictionaryToProcess = {\n ...dictionaryToProcess,\n content:\n excludeObjectFormat(\n dictionaryToProcess.content,\n targetLocaleDictionary.content\n ) ?? {},\n };\n }\n\n const localePreset = colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n formatLocale(targetLocale),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 18 }\n );\n\n appLogger(\n `${task.dictionaryPreset}${localePreset} Preparing ${colorizePath(basename(targetLocaleDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n dictionaryToProcess.content as unknown as Record<string, any>,\n CHUNK_SIZE\n );\n\n const nbOfChunks = chunkedJsonContent.length;\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset} Split into ${colorizeNumber(nbOfChunks)} chunks for translation`,\n {\n level: 'info',\n }\n );\n }\n\n const chunkResult: JsonChunk[] = [];\n\n // Process chunks in parallel (globally throttled) to allow concurrent translation\n const chunkPromises = chunkedJsonContent.map(async (chunk) => {\n const chunkPreset = createChunkPreset(chunk.index, chunk.total);\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`,\n {\n level: 'info',\n }\n );\n }\n\n const reconstructedChunk = reconstructFromSingleChunk(chunk);\n const {\n extractedContent: chunkExtractedContent,\n translatableDictionary: chunkTranslatableDictionary,\n } = extractTranslatableContent(reconstructedChunk);\n\n const executeTranslation = async () => {\n return await retryManager(\n async () => {\n let translationResult: any;\n\n if (aiClient && aiConfig) {\n translationResult = await aiClient.translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiConfig,\n });\n } else {\n translationResult = await intlayerAPI.ai\n .translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiOptions,\n })\n .then((result) => result.data);\n }\n\n if (!translationResult?.fileContent) {\n throw new Error('No content result');\n }\n\n const { isIdentic, error } = verifyIdenticObjectFormat(\n translationResult.fileContent,\n chunkTranslatableDictionary\n );\n\n if (!isIdentic) {\n throw new Error(\n `Translation result does not match expected format: ${error}`\n );\n }\n\n notifySuccess();\n return reinsertTranslatedContent(\n reconstructedChunk,\n chunkExtractedContent,\n translationResult.fileContent as Record<number, string>\n );\n },\n {\n maxRetry: MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) => {\n const chunkPreset = createChunkPreset(\n chunk.index,\n chunk.total\n );\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} ${colorize('Error filling:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n );\n\n followingErrors += 1;\n\n if (followingErrors >= MAX_FOLLOWING_ERRORS) {\n appLogger(`There is something wrong.`, {\n level: 'error',\n });\n process.exit(1); // 1 for error\n }\n },\n }\n )();\n };\n\n const wrapped = options?.onHandle\n ? options.onHandle(executeTranslation) // queued in global limiter\n : executeTranslation(); // no global limiter\n\n return wrapped.then((result) => ({ chunk, result }));\n });\n\n // Wait for all chunks for this locale in parallel (still capped by global limiter)\n const chunkResults = await Promise.all(chunkPromises);\n\n // Maintain order\n chunkResults\n .sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index)\n .forEach(({ result }) => {\n chunkResult.push(result);\n });\n\n // Merge translated chunk contents back into a single content object\n const reinsertedContent = mergeChunks(chunkResult);\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // Merge newly translated content (including explicit null fallbacks) back\n // into the existing target locale content. Applies to both per-locale and\n // multilingual paths so the target always retains previously translated\n // fields and receives null markers where the source has no translation.\n const finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n merged.content\n );\n\n return [targetLocale, finalContent] as const;\n })\n );\n\n const translatedContent: Partial<Record<Locale, Dictionary['content']>> =\n Object.fromEntries(translatedContentResults);\n\n const baseDictionary = baseUnmergedDictionary.locale\n ? {\n ...baseUnmergedDictionary,\n key: baseUnmergedDictionary.key!,\n content: {},\n }\n : baseUnmergedDictionary;\n\n let dictionaryOutput: Dictionary = {\n ...getMultilingualDictionary(baseDictionary),\n locale: undefined, // Ensure the dictionary is multilingual\n ...metadata,\n };\n\n for (const targetLocale of task.targetLocales) {\n if (translatedContent[targetLocale]) {\n dictionaryOutput = insertContentInDictionary(\n dictionaryOutput,\n translatedContent[targetLocale],\n targetLocale\n );\n }\n }\n\n appLogger(\n `${task.dictionaryPreset} ${colorize('Translation completed successfully', ANSIColors.GREEN)} for ${colorizePath(basename(dictionaryOutput.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n // The dict-level `fill` value may have been lost during JSON serialisation\n // (functions can't be serialised to JSON). Fall back to the config-level\n // fill so that an explicit fill in intlayer.config.ts is honoured.\n const effectiveFillForCheck =\n baseUnmergedDictionary.fill ?? configuration.dictionary?.fill;\n\n if (\n baseUnmergedDictionary.locale &&\n (effectiveFillForCheck === true ||\n effectiveFillForCheck === undefined) &&\n baseUnmergedDictionary.location === 'local'\n ) {\n const dictionaryFilePath = baseUnmergedDictionary\n .filePath!.split('.')\n .slice(0, -1);\n\n const contentIndex = dictionaryFilePath[dictionaryFilePath.length - 1];\n\n return JSON.parse(\n JSON.stringify({\n ...task,\n dictionaryOutput: {\n ...dictionaryOutput,\n fill: undefined,\n filled: true,\n },\n }).replaceAll(\n new RegExp(`\\\\.${contentIndex}\\\\.[a-zA-Z0-9]+`, 'g'),\n `.filled.${contentIndex}.json`\n )\n );\n }\n\n return {\n ...task,\n dictionaryOutput,\n };\n },\n {\n maxRetry: GROUP_MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Error:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n ),\n onMaxTryReached: ({ error }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Maximum number of retries reached:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)}`,\n {\n level: 'error',\n }\n ),\n }\n )();\n\n return result as TranslateDictionaryResult;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"translateDictionary.cjs","names":["ANSIColors","extractTranslatableContent","reinsertTranslatedContent","deepMergeContent"],"sources":["../../../src/fill/translateDictionary.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport type { AIConfig } from '@intlayer/ai';\nimport { type AIOptions, getIntlayerAPIProxy } from '@intlayer/api';\nimport {\n chunkJSON,\n excludeObjectFormat,\n formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n verifyIdenticObjectFormat,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n colorizePath,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { retryManager } from '@intlayer/config/utils';\nimport {\n getFilterMissingTranslationsDictionary,\n getMultilingualDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport type { AIClient } from '../utils/setupAI';\nimport { deepMergeContent } from './deepMergeContent';\nimport {\n extractTranslatableContent,\n reinsertTranslatedContent,\n} from './extractTranslatableContent';\nimport type { TranslationTask } from './listTranslationsTasks';\n\ntype TranslateDictionaryResult = TranslationTask & {\n dictionaryOutput: Dictionary | null;\n};\n\ntype TranslateDictionaryOptions = {\n mode: 'complete' | 'review';\n aiOptions?: AIOptions;\n fillMetadata?: boolean;\n onHandle?: ReturnType<\n typeof import('@intlayer/chokidar/utils').getGlobalLimiter\n >;\n onSuccess?: () => void;\n onError?: (error: unknown) => void;\n getAbortError?: () => Error | null;\n aiClient?: AIClient;\n aiConfig?: AIConfig;\n};\n\nconst createChunkPreset = (chunkIndex: number, totalChunks: number) => {\n if (totalChunks <= 1) return '';\n return colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n colorizeNumber(chunkIndex + 1),\n colorize(`/${totalChunks}`, ANSIColors.GREY_DARK),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 5 }\n );\n};\n\nconst hasMissingMetadata = (dictionary: Dictionary) =>\n !dictionary.description || !dictionary.title || !dictionary.tags;\n\nconst serializeError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.cause\n ? `${error.message} (cause: ${String(error.cause)})`\n : error.message;\n }\n if (typeof error === 'string') return error;\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n};\n\nconst CHUNK_SIZE = 4000;\nconst GROUP_MAX_RETRY = 2;\nconst MAX_RETRY = 3;\nconst RETRY_DELAY = 1000 * 10; // 10 seconds\n\nconst MAX_FOLLOWING_ERRORS = 10; // 10 errors in a row, hard exit the process\nlet followingErrors = 0;\n\nexport const translateDictionary = async (\n task: TranslationTask,\n configuration: IntlayerConfig,\n options?: TranslateDictionaryOptions\n): Promise<TranslateDictionaryResult> => {\n const appLogger = getAppLogger(configuration);\n const intlayerAPI = getIntlayerAPIProxy(undefined, configuration);\n\n const { mode, aiOptions, fillMetadata, aiClient, aiConfig } = {\n mode: 'complete',\n fillMetadata: true,\n ...options,\n } as const;\n\n const notifySuccess = () => {\n followingErrors = 0;\n options?.onSuccess?.();\n };\n\n const result = await retryManager(\n async () => {\n const unmergedDictionariesRecord = getUnmergedDictionaries(configuration);\n\n const baseUnmergedDictionary: Dictionary | undefined =\n unmergedDictionariesRecord[task.dictionaryKey].find(\n (dict) => dict.localId === task.dictionaryLocalId\n );\n\n if (!baseUnmergedDictionary) {\n appLogger(\n `${task.dictionaryPreset}Dictionary not found in unmergedDictionariesRecord. Skipping.`,\n {\n level: 'warn',\n }\n );\n return { ...task, dictionaryOutput: null };\n }\n\n let metadata:\n | Pick<Dictionary, 'description' | 'title' | 'tags'>\n | undefined;\n\n if (\n fillMetadata &&\n (hasMissingMetadata(baseUnmergedDictionary) || mode === 'review')\n ) {\n const defaultLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n configuration.internationalization.defaultLocale\n );\n\n appLogger(\n `${task.dictionaryPreset} Filling missing metadata for ${colorizePath(basename(baseUnmergedDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const runAudit = async () => {\n if (aiClient && aiConfig) {\n const result = await aiClient.auditDictionaryMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiConfig,\n });\n\n return {\n data: result,\n };\n }\n\n return await intlayerAPI.ai.auditContentDeclarationMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiOptions,\n });\n };\n\n const metadataResult = options?.onHandle\n ? await options.onHandle(runAudit)\n : await runAudit();\n\n metadata = metadataResult.data?.fileContent;\n }\n\n const translatedContentResults = await Promise.all(\n task.targetLocales.map(async (targetLocale) => {\n /**\n * In complete mode, for large dictionaries, we want to filter all content that is already translated\n *\n * targetLocale: fr\n *\n * { test1: t({ ar: 'Hello', en: 'Hello', fr: 'Bonjour' } }) -> {}\n * { test2: t({ ar: 'Hello', en: 'Hello' }) } -> { test2: t({ ar: 'Hello', en: 'Hello' }) }\n *\n */\n // Reset to base dictionary for each locale to ensure we filter from the original\n let dictionaryToProcess = structuredClone(baseUnmergedDictionary);\n\n let targetLocaleDictionary: Dictionary;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // For per-locale files, the content is already in simple JSON format (not translation nodes)\n // The base dictionary is already the source locale content\n\n // Look up the target locale dictionary by its locale property.\n // This handles both directory-based paths (/en/file.json → /es/file.json)\n // AND filename-based paths (messages_ICU/en.json → messages_ICU/es.json).\n // Plugins like syncJSON set `dict.locale` correctly, so matching by locale\n // is the only reliable strategy — path string-replacement fails when the\n // locale appears in the filename rather than a directory segment.\n const targetUnmergedDictionary = unmergedDictionariesRecord[\n task.dictionaryKey\n ]?.find(\n (dict) =>\n dict.locale === targetLocale &&\n dict.filePath !== baseUnmergedDictionary.filePath\n );\n\n // Derive the target file path for the fallback (when the file doesn't exist yet).\n // Try directory-style first (/en/ → /es/), then filename-style (en.json → es.json).\n const sourceFilePath = baseUnmergedDictionary.filePath ?? '';\n const derivedTargetFilePath = sourceFilePath.includes(\n `/${task.sourceLocale}/`\n )\n ? sourceFilePath.replace(\n new RegExp(`/${task.sourceLocale}/`, 'g'),\n `/${targetLocale}/`\n )\n : sourceFilePath.replace(\n new RegExp(`(^|/)${task.sourceLocale}(\\\\.[^/]+)$`),\n `$1${targetLocale}$2`\n );\n\n targetLocaleDictionary = targetUnmergedDictionary ?? {\n key: baseUnmergedDictionary.key,\n content: {},\n filePath: derivedTargetFilePath || undefined,\n locale: targetLocale,\n };\n } else {\n // For multilingual dictionaries\n if (mode === 'complete') {\n // Remove all nodes that don't have any content to translate\n dictionaryToProcess = getFilterMissingTranslationsDictionary(\n dictionaryToProcess,\n targetLocale\n );\n }\n\n dictionaryToProcess = getPerLocaleDictionary(\n dictionaryToProcess,\n task.sourceLocale\n );\n\n targetLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n targetLocale\n );\n }\n\n // Filter to only untranslated fields, preserving explicit null values as\n // default-locale fallback markers. Applied after both paths converge so\n // the same logic covers per-locale and multilingual dictionaries.\n if (mode === 'complete') {\n dictionaryToProcess = {\n ...dictionaryToProcess,\n content:\n excludeObjectFormat(\n dictionaryToProcess.content,\n targetLocaleDictionary.content\n ) ?? {},\n };\n }\n\n const localePreset = colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n formatLocale(targetLocale),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 18 }\n );\n\n appLogger(\n `${task.dictionaryPreset}${localePreset} Preparing ${colorizePath(basename(targetLocaleDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n dictionaryToProcess.content as unknown as Record<string, any>,\n CHUNK_SIZE\n );\n\n const nbOfChunks = chunkedJsonContent.length;\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset} Split into ${colorizeNumber(nbOfChunks)} chunks for translation`,\n {\n level: 'info',\n }\n );\n }\n\n const chunkResult: JsonChunk[] = [];\n\n // Process chunks in parallel (globally throttled) to allow concurrent translation\n const chunkPromises = chunkedJsonContent.map(async (chunk) => {\n const chunkPreset = createChunkPreset(chunk.index, chunk.total);\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`,\n {\n level: 'info',\n }\n );\n }\n\n const reconstructedChunk = reconstructFromSingleChunk(chunk);\n const {\n extractedContent: chunkExtractedContent,\n translatableDictionary: chunkTranslatableDictionary,\n } = extractTranslatableContent(reconstructedChunk);\n\n const executeTranslation = async () => {\n return await retryManager(\n async () => {\n let translationResult: any;\n\n if (aiClient && aiConfig) {\n translationResult = await aiClient.translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiConfig,\n });\n } else {\n translationResult = await intlayerAPI.ai\n .translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiOptions,\n })\n .then((result) => result.data);\n }\n\n if (!translationResult?.fileContent) {\n throw new Error('No content result');\n }\n\n const { isIdentic, error } = verifyIdenticObjectFormat(\n translationResult.fileContent,\n chunkTranslatableDictionary\n );\n\n if (!isIdentic) {\n throw new Error(\n `Translation result does not match expected format: ${error}`\n );\n }\n\n notifySuccess();\n return reinsertTranslatedContent(\n reconstructedChunk,\n chunkExtractedContent,\n translationResult.fileContent as Record<number, string>\n );\n },\n {\n maxRetry: MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) => {\n const chunkPreset = createChunkPreset(\n chunk.index,\n chunk.total\n );\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} ${colorize('Error filling:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n );\n\n followingErrors += 1;\n\n if (followingErrors >= MAX_FOLLOWING_ERRORS) {\n appLogger(`There is something wrong.`, {\n level: 'error',\n });\n process.exit(1); // 1 for error\n }\n },\n }\n )();\n };\n\n const wrapped = options?.onHandle\n ? options.onHandle(executeTranslation) // queued in global limiter\n : executeTranslation(); // no global limiter\n\n return wrapped.then((result) => ({ chunk, result }));\n });\n\n // Wait for all chunks for this locale in parallel (still capped by global limiter)\n const chunkResults = await Promise.all(chunkPromises);\n\n // Maintain order\n chunkResults\n .sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index)\n .forEach(({ result }) => {\n chunkResult.push(result);\n });\n\n // Merge translated chunk contents back into a single content object\n const reinsertedContent = mergeChunks(chunkResult);\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // Merge newly translated content (including explicit null fallbacks) back\n // into the existing target locale content. Applies to both per-locale and\n // multilingual paths so the target always retains previously translated\n // fields and receives null markers where the source has no translation.\n const finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n merged.content\n );\n\n return [targetLocale, finalContent] as const;\n })\n );\n\n const translatedContent: Partial<Record<Locale, Dictionary['content']>> =\n Object.fromEntries(translatedContentResults);\n\n const baseDictionary = baseUnmergedDictionary.locale\n ? {\n ...baseUnmergedDictionary,\n key: baseUnmergedDictionary.key!,\n content: {},\n }\n : baseUnmergedDictionary;\n\n let dictionaryOutput: Dictionary = {\n ...getMultilingualDictionary(baseDictionary),\n locale: undefined, // Ensure the dictionary is multilingual\n ...metadata,\n };\n\n for (const targetLocale of task.targetLocales) {\n if (translatedContent[targetLocale]) {\n dictionaryOutput = insertContentInDictionary(\n dictionaryOutput,\n translatedContent[targetLocale],\n targetLocale\n );\n }\n }\n\n appLogger(\n `${task.dictionaryPreset} ${colorize('Translation completed successfully', ANSIColors.GREEN)} for ${colorizePath(basename(dictionaryOutput.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n // The dict-level `fill` value may have been lost during JSON serialisation\n // (functions can't be serialised to JSON). Fall back to the config-level\n // fill so that an explicit fill in intlayer.config.ts is honoured.\n const effectiveFillForCheck =\n baseUnmergedDictionary.fill ?? configuration.dictionary?.fill;\n\n if (\n baseUnmergedDictionary.locale &&\n (effectiveFillForCheck === true ||\n effectiveFillForCheck === undefined) &&\n baseUnmergedDictionary.location === 'local'\n ) {\n const dictionaryFilePath = baseUnmergedDictionary\n .filePath!.split('.')\n .slice(0, -1);\n\n const contentIndex = dictionaryFilePath[dictionaryFilePath.length - 1];\n\n return JSON.parse(\n JSON.stringify({\n ...task,\n dictionaryOutput: {\n ...dictionaryOutput,\n fill: undefined,\n filled: true,\n },\n }).replaceAll(\n new RegExp(`\\\\.${contentIndex}\\\\.[a-zA-Z0-9]+`, 'g'),\n `.filled.${contentIndex}.json`\n )\n );\n }\n\n return {\n ...task,\n dictionaryOutput,\n };\n },\n {\n maxRetry: GROUP_MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Error:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n ),\n onMaxTryReached: ({ error }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Maximum number of retries reached:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)}`,\n {\n level: 'error',\n }\n ),\n }\n )();\n\n return result as TranslateDictionaryResult;\n};\n"],"mappings":"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"}
@@ -41,7 +41,7 @@ const writeFill = async (contentDeclarationFile, outputLocales, parentLocales, c
41
41
  filled: true,
42
42
  locale: output.isPerLocale ? output.localeList[0] : void 0,
43
43
  localId: `${contentDeclarationFile.key}::local::${output.filePath}`,
44
- filePath: (0, node_path.join)(configuration.system.baseDir, output.filePath)
44
+ filePath: (0, node_path.resolve)(configuration.system.baseDir, output.filePath)
45
45
  }, configuration, { localeList: output.isPerLocale ? output.localeList : outputLocales ?? configuration.internationalization.locales });
46
46
  if (output.isPerLocale) appLogger(`Auto filled per-locale content declaration for '${(0, _intlayer_config_logger.colorizeKey)(fullDictionary.key)}' written to ${(0, _intlayer_chokidar_utils.formatPath)(output.filePath)} for locale ${(0, _intlayer_chokidar_utils.formatLocale)(output.localeList[0])}`, { level: "info" });
47
47
  else appLogger(`Auto filled content declaration for '${(0, _intlayer_config_logger.colorizeKey)(fullDictionary.key)}' written to ${(0, _intlayer_chokidar_utils.formatPath)(output.filePath)}`, { level: "info" });
@@ -1 +1 @@
1
- {"version":3,"file":"writeFill.cjs","names":["getAvailableLocalesInDictionary","formatFillData"],"sources":["../../../src/fill/writeFill.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { writeContentDeclaration } from '@intlayer/chokidar/build';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary, Fill } from '@intlayer/types/dictionary';\nimport { type FillData, formatFillData } from './formatFillData';\nimport { getAvailableLocalesInDictionary } from './getAvailableLocalesInDictionary';\n\nexport const writeFill = async (\n contentDeclarationFile: Dictionary,\n outputLocales: Locale[],\n parentLocales: Locale[],\n configuration: IntlayerConfig\n) => {\n const appLogger = getAppLogger(configuration);\n const dictionaries = getDictionaries(configuration);\n\n const fullDictionary = dictionaries[contentDeclarationFile.key];\n\n const { filePath } = contentDeclarationFile;\n\n if (!filePath) {\n appLogger('No file path found for dictionary', {\n level: 'error',\n });\n return;\n }\n\n const fillOptions: Fill | undefined =\n contentDeclarationFile.fill ?? configuration.dictionary?.fill ?? true;\n\n if ((fillOptions as boolean) === false) {\n appLogger(\n `Auto fill is disabled for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const requestedLocales: Locale[] = (\n outputLocales ?? configuration.internationalization.locales\n ).filter((locale) => !parentLocales?.includes(locale));\n\n // Get locales that actually have translations in the content\n const availableLocales = getAvailableLocalesInDictionary(\n contentDeclarationFile\n );\n\n // Only write files for locales that have actual translations\n const localeList = requestedLocales.filter((locale) =>\n availableLocales.includes(locale)\n );\n\n if (localeList.length === 0) {\n appLogger(\n `No translations available for dictionary '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const fillData: FillData[] = await formatFillData(\n fillOptions as Fill,\n localeList,\n filePath,\n fullDictionary.key,\n configuration\n );\n\n for await (const output of fillData) {\n if (!output.filePath) {\n appLogger(\n `No file path found for auto filled content declaration for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'error',\n }\n );\n continue;\n }\n\n const { fill, ...rest } = contentDeclarationFile;\n\n await writeContentDeclaration(\n {\n ...rest,\n filled: true,\n locale: output.isPerLocale ? output.localeList[0] : undefined,\n localId: `${contentDeclarationFile.key}::local::${output.filePath}`,\n filePath: join(configuration.system.baseDir, output.filePath), // Use absolute path for vscode extension\n },\n configuration,\n {\n // Per-locale files: write only the specific locale.\n // Multilingual files (string/function fill, single output): include all\n // output locales (including source) so the file is complete.\n localeList: output.isPerLocale\n ? output.localeList\n : (outputLocales ?? configuration.internationalization.locales),\n }\n );\n\n if (output.isPerLocale) {\n appLogger(\n `Auto filled per-locale content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)} for locale ${formatLocale(output.localeList[0])}`,\n { level: 'info' }\n );\n } else {\n appLogger(\n `Auto filled content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)}`,\n { level: 'info' }\n );\n }\n }\n};\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,YAAY,OACvB,wBACA,eACA,eACA,kBACG;CACH,MAAM,sDAAyB,cAAc;CAG7C,MAAM,mEAF+B,cAEF,CAAC,uBAAuB;CAE3D,MAAM,EAAE,aAAa;AAErB,KAAI,CAAC,UAAU;AACb,YAAU,qCAAqC,EAC7C,OAAO,SACR,CAAC;AACF;;CAGF,MAAM,cACJ,uBAAuB,QAAQ,cAAc,YAAY,QAAQ;AAEnE,KAAK,gBAA4B,OAAO;AACtC,YACE,uEAA0C,eAAe,IAAI,CAAC,IAC9D,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,oBACJ,iBAAiB,cAAc,qBAAqB,SACpD,QAAQ,WAAW,CAAC,eAAe,SAAS,OAAO,CAAC;CAGtD,MAAM,mBAAmBA,6EACvB,uBACD;CAGD,MAAM,aAAa,iBAAiB,QAAQ,WAC1C,iBAAiB,SAAS,OAAO,CAClC;AAED,KAAI,WAAW,WAAW,GAAG;AAC3B,YACE,sFAAyD,eAAe,IAAI,CAAC,IAC7E,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,WAAuB,MAAMC,2CACjC,aACA,YACA,UACA,eAAe,KACf,cACD;AAED,YAAW,MAAM,UAAU,UAAU;AACnC,MAAI,CAAC,OAAO,UAAU;AACpB,aACE,wGAA2E,eAAe,IAAI,CAAC,IAC/F,EACE,OAAO,SACR,CACF;AACD;;EAGF,MAAM,EAAE,MAAM,GAAG,SAAS;AAE1B,8DACE;GACE,GAAG;GACH,QAAQ;GACR,QAAQ,OAAO,cAAc,OAAO,WAAW,KAAK;GACpD,SAAS,GAAG,uBAAuB,IAAI,WAAW,OAAO;GACzD,8BAAe,cAAc,OAAO,SAAS,OAAO,SAAS;GAC9D,EACD,eACA,EAIE,YAAY,OAAO,cACf,OAAO,aACN,iBAAiB,cAAc,qBAAqB,SAC1D,CACF;AAED,MAAI,OAAO,YACT,WACE,4FAA+D,eAAe,IAAI,CAAC,wDAA0B,OAAO,SAAS,CAAC,yDAA2B,OAAO,WAAW,GAAG,IAC9K,EAAE,OAAO,QAAQ,CAClB;MAED,WACE,iFAAoD,eAAe,IAAI,CAAC,wDAA0B,OAAO,SAAS,IAClH,EAAE,OAAO,QAAQ,CAClB"}
1
+ {"version":3,"file":"writeFill.cjs","names":["getAvailableLocalesInDictionary","formatFillData"],"sources":["../../../src/fill/writeFill.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { writeContentDeclaration } from '@intlayer/chokidar/build';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary, Fill } from '@intlayer/types/dictionary';\nimport { type FillData, formatFillData } from './formatFillData';\nimport { getAvailableLocalesInDictionary } from './getAvailableLocalesInDictionary';\n\nexport const writeFill = async (\n contentDeclarationFile: Dictionary,\n outputLocales: Locale[],\n parentLocales: Locale[],\n configuration: IntlayerConfig\n) => {\n const appLogger = getAppLogger(configuration);\n const dictionaries = getDictionaries(configuration);\n\n const fullDictionary = dictionaries[contentDeclarationFile.key];\n\n const { filePath } = contentDeclarationFile;\n\n if (!filePath) {\n appLogger('No file path found for dictionary', {\n level: 'error',\n });\n return;\n }\n\n const fillOptions: Fill | undefined =\n contentDeclarationFile.fill ?? configuration.dictionary?.fill ?? true;\n\n if ((fillOptions as boolean) === false) {\n appLogger(\n `Auto fill is disabled for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const requestedLocales: Locale[] = (\n outputLocales ?? configuration.internationalization.locales\n ).filter((locale) => !parentLocales?.includes(locale));\n\n // Get locales that actually have translations in the content\n const availableLocales = getAvailableLocalesInDictionary(\n contentDeclarationFile\n );\n\n // Only write files for locales that have actual translations\n const localeList = requestedLocales.filter((locale) =>\n availableLocales.includes(locale)\n );\n\n if (localeList.length === 0) {\n appLogger(\n `No translations available for dictionary '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const fillData: FillData[] = await formatFillData(\n fillOptions as Fill,\n localeList,\n filePath,\n fullDictionary.key,\n configuration\n );\n\n for await (const output of fillData) {\n if (!output.filePath) {\n appLogger(\n `No file path found for auto filled content declaration for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'error',\n }\n );\n continue;\n }\n\n const { fill, ...rest } = contentDeclarationFile;\n\n await writeContentDeclaration(\n {\n ...rest,\n filled: true,\n locale: output.isPerLocale ? output.localeList[0] : undefined,\n localId: `${contentDeclarationFile.key}::local::${output.filePath}`,\n filePath: resolve(configuration.system.baseDir, output.filePath), // Use absolute path for vscode extension\n },\n configuration,\n {\n // Per-locale files: write only the specific locale.\n // Multilingual files (string/function fill, single output): include all\n // output locales (including source) so the file is complete.\n localeList: output.isPerLocale\n ? output.localeList\n : (outputLocales ?? configuration.internationalization.locales),\n }\n );\n\n if (output.isPerLocale) {\n appLogger(\n `Auto filled per-locale content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)} for locale ${formatLocale(output.localeList[0])}`,\n { level: 'info' }\n );\n } else {\n appLogger(\n `Auto filled content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)}`,\n { level: 'info' }\n );\n }\n }\n};\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,YAAY,OACvB,wBACA,eACA,eACA,kBACG;CACH,MAAM,sDAAyB,cAAc;CAG7C,MAAM,mEAF+B,cAEF,CAAC,uBAAuB;CAE3D,MAAM,EAAE,aAAa;AAErB,KAAI,CAAC,UAAU;AACb,YAAU,qCAAqC,EAC7C,OAAO,SACR,CAAC;AACF;;CAGF,MAAM,cACJ,uBAAuB,QAAQ,cAAc,YAAY,QAAQ;AAEnE,KAAK,gBAA4B,OAAO;AACtC,YACE,uEAA0C,eAAe,IAAI,CAAC,IAC9D,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,oBACJ,iBAAiB,cAAc,qBAAqB,SACpD,QAAQ,WAAW,CAAC,eAAe,SAAS,OAAO,CAAC;CAGtD,MAAM,mBAAmBA,6EACvB,uBACD;CAGD,MAAM,aAAa,iBAAiB,QAAQ,WAC1C,iBAAiB,SAAS,OAAO,CAClC;AAED,KAAI,WAAW,WAAW,GAAG;AAC3B,YACE,sFAAyD,eAAe,IAAI,CAAC,IAC7E,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,WAAuB,MAAMC,2CACjC,aACA,YACA,UACA,eAAe,KACf,cACD;AAED,YAAW,MAAM,UAAU,UAAU;AACnC,MAAI,CAAC,OAAO,UAAU;AACpB,aACE,wGAA2E,eAAe,IAAI,CAAC,IAC/F,EACE,OAAO,SACR,CACF;AACD;;EAGF,MAAM,EAAE,MAAM,GAAG,SAAS;AAE1B,8DACE;GACE,GAAG;GACH,QAAQ;GACR,QAAQ,OAAO,cAAc,OAAO,WAAW,KAAK;GACpD,SAAS,GAAG,uBAAuB,IAAI,WAAW,OAAO;GACzD,iCAAkB,cAAc,OAAO,SAAS,OAAO,SAAS;GACjE,EACD,eACA,EAIE,YAAY,OAAO,cACf,OAAO,aACN,iBAAiB,cAAc,qBAAqB,SAC1D,CACF;AAED,MAAI,OAAO,YACT,WACE,4FAA+D,eAAe,IAAI,CAAC,wDAA0B,OAAO,SAAS,CAAC,yDAA2B,OAAO,WAAW,GAAG,IAC9K,EAAE,OAAO,QAAQ,CAClB;MAED,WACE,iFAAoD,eAAe,IAAI,CAAC,wDAA0B,OAAO,SAAS,IAClH,EAAE,OAAO,QAAQ,CAClB"}
@@ -37,7 +37,7 @@ const fill = async (options) => {
37
37
  }
38
38
  }
39
39
  const targetUnmergedDictionaries = await getTargetUnmergedDictionaries(options);
40
- const sourceDictionaries = await loadContentDeclarations([...new Set(targetUnmergedDictionaries.filter((unmergedDictionary) => unmergedDictionary.location !== "remote").map((unmergedDictionary) => unmergedDictionary.filePath).filter(Boolean))].map((sourcePath) => join(configuration.system.baseDir, sourcePath)), configuration, void 0, { logError: false });
40
+ const sourceDictionaries = await loadContentDeclarations([...new Set(targetUnmergedDictionaries.filter((unmergedDictionary) => ["local", "hybrid"].includes(unmergedDictionary.location)).map((unmergedDictionary) => unmergedDictionary.filePath).filter(Boolean))].map((sourcePath) => join(configuration.system.baseDir, sourcePath)), configuration, void 0, { logError: false });
41
41
  const originalFillByPath = /* @__PURE__ */ new Map();
42
42
  for (const dictionary of sourceDictionaries) if (dictionary.filePath) originalFillByPath.set(dictionary.filePath, dictionary.fill);
43
43
  const affectedDictionaryKeys = /* @__PURE__ */ new Set();
@@ -75,7 +75,7 @@ const fill = async (options) => {
75
75
  const dictFill = originalFill !== void 0 ? originalFill : dictionaryOutput.fill;
76
76
  const hasDictionaryLevelFill = typeof dictFill === "string" || typeof dictFill === "function" || typeof dictFill === "object" && dictFill !== null;
77
77
  const isPerLocale = typeof dictionaryOutput.locale === "string";
78
- const effectiveFill = hasDictionaryLevelFill ? dictFill : isPerLocale ? configuration.dictionary?.fill ?? true : configuration.dictionary?.fill ?? false;
78
+ const effectiveFill = hasDictionaryLevelFill ? dictFill : isPerLocale ? configuration.dictionary?.fill ?? true : false;
79
79
  if (typeof effectiveFill === "string" || typeof effectiveFill === "function" || typeof effectiveFill === "object" && effectiveFill !== null) await writeFill({
80
80
  ...dictionaryOutput,
81
81
  fill: effectiveFill
@@ -1 +1 @@
1
- {"version":3,"file":"fill.mjs","names":[],"sources":["../../../src/fill/fill.ts"],"sourcesContent":["import { basename, join, relative } from 'node:path';\n\nimport type { AIOptions } from '@intlayer/api';\nimport {\n loadContentDeclarations,\n prepareIntlayer,\n writeContentDeclaration,\n} from '@intlayer/chokidar/build';\nimport {\n type ListGitFilesOptions,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport {\n formatPath,\n getGlobalLimiter,\n getTaskLimiter,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizePath,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { Fill } from '@intlayer/types/dictionary';\nimport {\n ensureArray,\n type GetTargetDictionaryOptions,\n getTargetUnmergedDictionaries,\n} from '../getTargetDictionary';\nimport { setupAI } from '../utils/setupAI';\nimport {\n listTranslationsTasks,\n type TranslationTask,\n} from './listTranslationsTasks';\nimport { translateDictionary } from './translateDictionary';\nimport { writeFill } from './writeFill';\n\nconst NB_CONCURRENT_TRANSLATIONS = 7;\n\n// Arguments for the fill function\nexport type FillOptions = {\n sourceLocale?: Locale;\n outputLocales?: Locale | Locale[];\n mode?: 'complete' | 'review';\n gitOptions?: ListGitFilesOptions;\n aiOptions?: AIOptions; // Added aiOptions to be passed to translateJSON\n verbose?: boolean;\n nbConcurrentTranslations?: number;\n nbConcurrentTasks?: number; // NEW: number of tasks that may run at once\n build?: boolean;\n skipMetadata?: boolean;\n} & GetTargetDictionaryOptions;\n\n/**\n * Fill translations based on the provided options.\n */\nexport const fill = async (options?: FillOptions): Promise<void> => {\n const configuration = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n const appLogger = getAppLogger(configuration);\n\n if (options?.build === true) {\n await prepareIntlayer(configuration, { forceRun: true });\n } else if (typeof options?.build === 'undefined') {\n await prepareIntlayer(configuration);\n }\n\n const { defaultLocale, locales } = configuration.internationalization;\n const mode = options?.mode ?? 'complete';\n const baseLocale = options?.sourceLocale ?? defaultLocale;\n\n const outputLocales = options?.outputLocales\n ? ensureArray(options.outputLocales)\n : locales;\n\n const aiResult = await setupAI(configuration, options?.aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig, isCustomAI } = aiResult;\n\n if (isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n\n const targetUnmergedDictionaries =\n await getTargetUnmergedDictionaries(options);\n\n // Load the original source content declaration files to recover function-type\n // `fill` values that are lost when dictionaries are JSON-serialised into\n // unmerged_dictionaries.cjs. Dictionary-level fill takes priority over the\n // config-level fill, but we can only know that by reading the source files.\n const uniqueSourcePaths = [\n ...new Set(\n targetUnmergedDictionaries\n .filter(\n (unmergedDictionary) => unmergedDictionary.location !== 'remote'\n )\n .map((unmergedDictionary) => unmergedDictionary.filePath)\n .filter(Boolean) as string[]\n ),\n ];\n const sourceDictionaries = await loadContentDeclarations(\n uniqueSourcePaths.map((sourcePath) =>\n join(configuration.system.baseDir, sourcePath)\n ),\n configuration,\n undefined,\n {\n logError: false,\n }\n );\n // Map relative filePath → original fill value from the source file\n const originalFillByPath = new Map<string, Fill | undefined>();\n\n for (const dictionary of sourceDictionaries) {\n if (dictionary.filePath) {\n originalFillByPath.set(\n dictionary.filePath,\n dictionary.fill as Fill | undefined\n );\n }\n }\n\n const affectedDictionaryKeys = new Set<string>();\n\n targetUnmergedDictionaries.forEach((dict) => {\n affectedDictionaryKeys.add(dict.key);\n });\n\n const keysToProcess = Array.from(affectedDictionaryKeys);\n\n appLogger([\n 'Affected dictionary keys for processing:',\n keysToProcess.length > 0\n ? keysToProcess.map((key) => colorizeKey(key)).join(', ')\n : colorize('No keys found', ANSIColors.YELLOW),\n ]);\n\n if (keysToProcess.length === 0) return;\n\n /**\n * List the translations tasks\n *\n * Create a list of per-locale dictionaries to translate\n *\n * In 'complete' mode, filter only the missing locales to translate\n */\n const translationTasks: TranslationTask[] = listTranslationsTasks(\n targetUnmergedDictionaries.map((dictionary) => dictionary.localId!),\n outputLocales,\n mode,\n baseLocale,\n configuration\n );\n\n // AI calls in flight at once (translateJSON + metadata audit)\n const nbConcurrentTranslations =\n options?.nbConcurrentTranslations ?? NB_CONCURRENT_TRANSLATIONS;\n const globalLimiter = getGlobalLimiter(nbConcurrentTranslations);\n\n // NEW: number of *tasks* that may run at once (start/prepare/log/write)\n const nbConcurrentTasks = Math.max(\n 1,\n Math.min(\n options?.nbConcurrentTasks ?? nbConcurrentTranslations,\n translationTasks.length\n )\n );\n\n const taskLimiter = getTaskLimiter(nbConcurrentTasks);\n\n const runners = translationTasks.map((task) =>\n taskLimiter(async () => {\n const relativePath = relative(\n configuration?.system?.baseDir ?? process.cwd(),\n task?.dictionaryFilePath ?? ''\n );\n\n // log AFTER acquiring a task slot\n appLogger(\n `${task.dictionaryPreset} Processing ${colorizePath(basename(relativePath))}`,\n { level: 'info' }\n );\n\n const translationTaskResult = await translateDictionary(\n task,\n configuration,\n {\n mode,\n aiOptions: options?.aiOptions,\n fillMetadata: !options?.skipMetadata,\n onHandle: globalLimiter,\n aiClient,\n aiConfig,\n }\n );\n\n if (!translationTaskResult?.dictionaryOutput) return;\n\n const { dictionaryOutput, sourceLocale } = translationTaskResult;\n\n // Determine if we should write to separate files\n // - If dictionary has explicit fill setting (string, function, or object), use it\n // - If dictionary is per-locale AND has no explicit fill=false, use global fill config\n // - If dictionary is multilingual (no locale property), always write to same file\n //\n // NOTE: function-type fill values are lost during JSON serialisation of\n // unmerged_dictionaries.cjs. We recover them by checking the original\n // source file that was loaded above (originalFillByPath). Dictionary-level\n // fill always takes priority over config-level fill.\n const originalFill = originalFillByPath.get(\n dictionaryOutput.filePath ?? ''\n );\n\n // originalFill is undefined when the source file had no fill property; use\n // the (possibly JSON-preserved) dictionaryOutput.fill as a fallback so that\n // string/boolean fill values set directly on the dict still work.\n const dictFill: Fill | undefined =\n originalFill !== undefined ? originalFill : dictionaryOutput.fill;\n\n const hasDictionaryLevelFill =\n typeof dictFill === 'string' ||\n typeof dictFill === 'function' ||\n (typeof dictFill === 'object' && dictFill !== null);\n\n const isPerLocale = typeof dictionaryOutput.locale === 'string';\n\n const effectiveFill = hasDictionaryLevelFill\n ? dictFill\n : isPerLocale\n ? (configuration.dictionary?.fill ?? true)\n : (configuration.dictionary?.fill ?? false); // Multilingual dictionaries use config-level fill if set\n\n const isFillOtherFile =\n typeof effectiveFill === 'string' ||\n typeof effectiveFill === 'function' ||\n (typeof effectiveFill === 'object' && effectiveFill !== null);\n\n if (isFillOtherFile) {\n await writeFill(\n {\n ...dictionaryOutput,\n // Ensure fill is set on the dictionary for writeFill to use\n fill: effectiveFill,\n },\n outputLocales,\n [sourceLocale],\n configuration\n );\n } else {\n await writeContentDeclaration(dictionaryOutput, configuration);\n\n if (dictionaryOutput.filePath) {\n appLogger(\n `${task.dictionaryPreset} Content declaration written to ${formatPath(basename(dictionaryOutput.filePath))}`,\n { level: 'info' }\n );\n }\n }\n })\n );\n\n await Promise.all(runners);\n await (globalLimiter as any).onIdle();\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"fill.mjs","names":[],"sources":["../../../src/fill/fill.ts"],"sourcesContent":["import { basename, join, relative } from 'node:path';\n\nimport type { AIOptions } from '@intlayer/api';\nimport {\n loadContentDeclarations,\n prepareIntlayer,\n writeContentDeclaration,\n} from '@intlayer/chokidar/build';\nimport {\n type ListGitFilesOptions,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport {\n formatPath,\n getGlobalLimiter,\n getTaskLimiter,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizePath,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { Fill } from '@intlayer/types/dictionary';\nimport {\n ensureArray,\n type GetTargetDictionaryOptions,\n getTargetUnmergedDictionaries,\n} from '../getTargetDictionary';\nimport { setupAI } from '../utils/setupAI';\nimport {\n listTranslationsTasks,\n type TranslationTask,\n} from './listTranslationsTasks';\nimport { translateDictionary } from './translateDictionary';\nimport { writeFill } from './writeFill';\n\nconst NB_CONCURRENT_TRANSLATIONS = 7;\n\n// Arguments for the fill function\nexport type FillOptions = {\n sourceLocale?: Locale;\n outputLocales?: Locale | Locale[];\n mode?: 'complete' | 'review';\n gitOptions?: ListGitFilesOptions;\n aiOptions?: AIOptions; // Added aiOptions to be passed to translateJSON\n verbose?: boolean;\n nbConcurrentTranslations?: number;\n nbConcurrentTasks?: number; // NEW: number of tasks that may run at once\n build?: boolean;\n skipMetadata?: boolean;\n} & GetTargetDictionaryOptions;\n\n/**\n * Fill translations based on the provided options.\n */\nexport const fill = async (options?: FillOptions): Promise<void> => {\n const configuration = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n const appLogger = getAppLogger(configuration);\n\n if (options?.build === true) {\n await prepareIntlayer(configuration, { forceRun: true });\n } else if (typeof options?.build === 'undefined') {\n await prepareIntlayer(configuration);\n }\n\n const { defaultLocale, locales } = configuration.internationalization;\n const mode = options?.mode ?? 'complete';\n const baseLocale = options?.sourceLocale ?? defaultLocale;\n\n const outputLocales = options?.outputLocales\n ? ensureArray(options.outputLocales)\n : locales;\n\n const aiResult = await setupAI(configuration, options?.aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig, isCustomAI } = aiResult;\n\n if (isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n\n const targetUnmergedDictionaries =\n await getTargetUnmergedDictionaries(options);\n\n // Load the original source content declaration files to recover function-type\n // `fill` values that are lost when dictionaries are JSON-serialised into\n // unmerged_dictionaries.cjs. Dictionary-level fill takes priority over the\n // config-level fill, but we can only know that by reading the source files.\n const uniqueSourcePaths = [\n ...new Set(\n targetUnmergedDictionaries\n .filter((unmergedDictionary) =>\n ['local', 'hybrid'].includes(unmergedDictionary.location!)\n )\n .map((unmergedDictionary) => unmergedDictionary.filePath)\n .filter(Boolean) as string[]\n ),\n ];\n const sourceDictionaries = await loadContentDeclarations(\n uniqueSourcePaths.map((sourcePath) =>\n join(configuration.system.baseDir, sourcePath)\n ),\n configuration,\n undefined,\n {\n logError: false,\n }\n );\n // Map relative filePath → original fill value from the source file\n const originalFillByPath = new Map<string, Fill | undefined>();\n\n for (const dictionary of sourceDictionaries) {\n if (dictionary.filePath) {\n originalFillByPath.set(\n dictionary.filePath,\n dictionary.fill as Fill | undefined\n );\n }\n }\n\n const affectedDictionaryKeys = new Set<string>();\n\n targetUnmergedDictionaries.forEach((dict) => {\n affectedDictionaryKeys.add(dict.key);\n });\n\n const keysToProcess = Array.from(affectedDictionaryKeys);\n\n appLogger([\n 'Affected dictionary keys for processing:',\n keysToProcess.length > 0\n ? keysToProcess.map((key) => colorizeKey(key)).join(', ')\n : colorize('No keys found', ANSIColors.YELLOW),\n ]);\n\n if (keysToProcess.length === 0) return;\n\n /**\n * List the translations tasks\n *\n * Create a list of per-locale dictionaries to translate\n *\n * In 'complete' mode, filter only the missing locales to translate\n */\n const translationTasks: TranslationTask[] = listTranslationsTasks(\n targetUnmergedDictionaries.map((dictionary) => dictionary.localId!),\n outputLocales,\n mode,\n baseLocale,\n configuration\n );\n\n // AI calls in flight at once (translateJSON + metadata audit)\n const nbConcurrentTranslations =\n options?.nbConcurrentTranslations ?? NB_CONCURRENT_TRANSLATIONS;\n const globalLimiter = getGlobalLimiter(nbConcurrentTranslations);\n\n // NEW: number of *tasks* that may run at once (start/prepare/log/write)\n const nbConcurrentTasks = Math.max(\n 1,\n Math.min(\n options?.nbConcurrentTasks ?? nbConcurrentTranslations,\n translationTasks.length\n )\n );\n\n const taskLimiter = getTaskLimiter(nbConcurrentTasks);\n\n const runners = translationTasks.map((task) =>\n taskLimiter(async () => {\n const relativePath = relative(\n configuration?.system?.baseDir ?? process.cwd(),\n task?.dictionaryFilePath ?? ''\n );\n\n // log AFTER acquiring a task slot\n appLogger(\n `${task.dictionaryPreset} Processing ${colorizePath(basename(relativePath))}`,\n { level: 'info' }\n );\n\n const translationTaskResult = await translateDictionary(\n task,\n configuration,\n {\n mode,\n aiOptions: options?.aiOptions,\n fillMetadata: !options?.skipMetadata,\n onHandle: globalLimiter,\n aiClient,\n aiConfig,\n }\n );\n\n if (!translationTaskResult?.dictionaryOutput) return;\n\n const { dictionaryOutput, sourceLocale } = translationTaskResult;\n\n // Determine if we should write to separate files\n // - If dictionary has explicit fill setting (string, function, or object), use it\n // - If dictionary is per-locale AND has no explicit fill=false, use global fill config\n // - If dictionary is multilingual (no locale property), always write to same file\n //\n // NOTE: function-type fill values are lost during JSON serialisation of\n // unmerged_dictionaries.cjs. We recover them by checking the original\n // source file that was loaded above (originalFillByPath). Dictionary-level\n // fill always takes priority over config-level fill.\n const originalFill = originalFillByPath.get(\n dictionaryOutput.filePath ?? ''\n );\n\n // originalFill is undefined when the source file had no fill property; use\n // the (possibly JSON-preserved) dictionaryOutput.fill as a fallback so that\n // string/boolean fill values set directly on the dict still work.\n const dictFill: Fill | undefined =\n originalFill !== undefined ? originalFill : dictionaryOutput.fill;\n\n const hasDictionaryLevelFill =\n typeof dictFill === 'string' ||\n typeof dictFill === 'function' ||\n (typeof dictFill === 'object' && dictFill !== null);\n\n const isPerLocale = typeof dictionaryOutput.locale === 'string';\n\n const effectiveFill = hasDictionaryLevelFill\n ? dictFill\n : isPerLocale\n ? (configuration.dictionary?.fill ?? true)\n : false; // Multilingual dictionaries (no locale property) always write to same file unless explicitly set on the dictionary\n\n const isFillOtherFile =\n typeof effectiveFill === 'string' ||\n typeof effectiveFill === 'function' ||\n (typeof effectiveFill === 'object' && effectiveFill !== null);\n\n if (isFillOtherFile) {\n await writeFill(\n {\n ...dictionaryOutput,\n // Ensure fill is set on the dictionary for writeFill to use\n fill: effectiveFill,\n },\n outputLocales,\n [sourceLocale],\n configuration\n );\n } else {\n await writeContentDeclaration(dictionaryOutput, configuration);\n\n if (dictionaryOutput.filePath) {\n appLogger(\n `${task.dictionaryPreset} Content declaration written to ${formatPath(basename(dictionaryOutput.filePath))}`,\n { level: 'info' }\n );\n }\n }\n })\n );\n\n await Promise.all(runners);\n await (globalLimiter as any).onIdle();\n};\n"],"mappings":"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"}
@@ -1,16 +1,27 @@
1
1
  import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
2
+ import { getFormatFromExtension } from "@intlayer/chokidar/utils";
3
+ import { GREY_DARK } from "@intlayer/config/colors";
4
+ import { colorize, colorizePath } from "@intlayer/config/logger";
2
5
  import { parseFilePathPattern } from "@intlayer/config/utils";
3
6
 
4
7
  //#region src/fill/formatAutoFilledFilePath.ts
5
- const formatAutoFilledFilePath = async (autoFillField, dictionaryKey, dictionaryFilePath, baseDir, locale) => {
8
+ const formatAutoFilledFilePath = async (autoFillField, dictionaryKey, dictionaryFilePath, baseDir, configuration, locale) => {
6
9
  if (!autoFillField) throw new Error("autoFillField must be provided");
7
10
  if (!dictionaryKey || typeof dictionaryKey !== "string") throw new Error("dictionaryKey must be a non-empty string");
8
11
  if (!dictionaryFilePath || typeof dictionaryFilePath !== "string") throw new Error("dictionaryFilePath must be a non-empty string");
9
12
  if (!baseDir || typeof baseDir !== "string") throw new Error("baseDir must be a non-empty string");
13
+ const base = basename(dictionaryFilePath);
14
+ const { fileExtensions } = configuration.content;
15
+ const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));
16
+ const originalFileName = extensionMatch ? base.slice(0, -extensionMatch.length) : base.split(".")[0];
17
+ console.log({ extensionMatch });
18
+ if (!extensionMatch) throw new Error(`No extension found for file ${colorizePath(dictionaryFilePath)}. Valid extensions are: ${colorize(fileExtensions.join(", "), GREY_DARK)}`);
10
19
  const result = await parseFilePathPattern(autoFillField, {
11
20
  key: dictionaryKey,
12
- fileName: basename(dictionaryFilePath).split(".").slice(0, -2).join("."),
13
- locale
21
+ fileName: originalFileName,
22
+ locale,
23
+ extension: extensionMatch,
24
+ format: getFormatFromExtension(extensionMatch)
14
25
  });
15
26
  const absoluteDictionaryPath = isAbsolute(dictionaryFilePath) ? dictionaryFilePath : resolve(baseDir, dictionaryFilePath);
16
27
  if (result.startsWith("./") || result.startsWith("../")) return resolve(dirname(absoluteDictionaryPath), result);
@@ -1 +1 @@
1
- {"version":3,"file":"formatAutoFilledFilePath.mjs","names":[],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"sourcesContent":["import { basename, dirname, isAbsolute, normalize, resolve } from 'node:path';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\n\nexport const formatAutoFilledFilePath = async (\n autoFillField: FilePathPattern,\n dictionaryKey: string,\n dictionaryFilePath: string,\n baseDir: string,\n locale?: Locale\n): Promise<string> => {\n // Validate inputs\n if (!autoFillField) {\n throw new Error('autoFillField must be provided');\n }\n if (!dictionaryKey || typeof dictionaryKey !== 'string') {\n throw new Error('dictionaryKey must be a non-empty string');\n }\n if (!dictionaryFilePath || typeof dictionaryFilePath !== 'string') {\n throw new Error('dictionaryFilePath must be a non-empty string');\n }\n if (!baseDir || typeof baseDir !== 'string') {\n throw new Error('baseDir must be a non-empty string');\n }\n\n // Extract the original filename without extensions (.content.ts -> dictionaryFieldEditor)\n const originalFileName = basename(dictionaryFilePath)\n .split('.')\n .slice(0, -2) // Remove last 2 extensions (.content.tsx)\n .join('.');\n\n // Replace placeholders in autoFillField\n const result: string = await parseFilePathPattern(autoFillField, {\n key: dictionaryKey,\n fileName: originalFileName,\n locale,\n });\n\n // Normalize the dictionary file path - if it's relative, make it absolute relative to baseDir\n const absoluteDictionaryPath = isAbsolute(dictionaryFilePath)\n ? dictionaryFilePath\n : resolve(baseDir, dictionaryFilePath);\n\n // Handle relative paths (starting with ./ or ../)\n if (result.startsWith('./') || result.startsWith('../')) {\n const fileDir = dirname(absoluteDictionaryPath);\n const resolvedPath = resolve(fileDir, result);\n\n return resolvedPath;\n }\n\n // Handle absolute paths\n if (isAbsolute(result)) {\n const normalizedResult = normalize(result);\n const normalizedBaseDir = normalize(baseDir);\n\n // Check if it's relative to baseDir (starts with /)\n // but not a system path (like /usr/local)\n if (\n result.startsWith('/') &&\n !normalizedResult.startsWith(normalizedBaseDir)\n ) {\n // Try to resolve it relative to baseDir first\n const relativeToBase = resolve(baseDir, result.substring(1));\n\n // If the path doesn't exist in common system directories, treat as relative to baseDir\n if (\n !result.startsWith('/usr/') &&\n !result.startsWith('/etc/') &&\n !result.startsWith('/var/') &&\n !result.startsWith('/home/') &&\n !result.startsWith('/Users/')\n ) {\n return relativeToBase;\n }\n }\n\n // It's a true system absolute path\n return normalizedResult;\n }\n\n // Default case: treat as relative to baseDir\n return normalize(result);\n};\n"],"mappings":";;;;AAKA,MAAa,2BAA2B,OACtC,eACA,eACA,oBACA,SACA,WACoB;AAEpB,KAAI,CAAC,cACH,OAAM,IAAI,MAAM,iCAAiC;AAEnD,KAAI,CAAC,iBAAiB,OAAO,kBAAkB,SAC7C,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,CAAC,sBAAsB,OAAO,uBAAuB,SACvD,OAAM,IAAI,MAAM,gDAAgD;AAElE,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,qCAAqC;CAUvD,MAAM,SAAiB,MAAM,qBAAqB,eAAe;EAC/D,KAAK;EACL,UARuB,SAAS,mBAAmB,CAClD,MAAM,IAAI,CACV,MAAM,GAAG,GAAG,CACZ,KAAK,IAKoB;EAC1B;EACD,CAAC;CAGF,MAAM,yBAAyB,WAAW,mBAAmB,GACzD,qBACA,QAAQ,SAAS,mBAAmB;AAGxC,KAAI,OAAO,WAAW,KAAK,IAAI,OAAO,WAAW,MAAM,CAIrD,QAFqB,QADL,QAAQ,uBACY,EAAE,OAEnB;AAIrB,KAAI,WAAW,OAAO,EAAE;EACtB,MAAM,mBAAmB,UAAU,OAAO;EAC1C,MAAM,oBAAoB,UAAU,QAAQ;AAI5C,MACE,OAAO,WAAW,IAAI,IACtB,CAAC,iBAAiB,WAAW,kBAAkB,EAC/C;GAEA,MAAM,iBAAiB,QAAQ,SAAS,OAAO,UAAU,EAAE,CAAC;AAG5D,OACE,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,SAAS,IAC5B,CAAC,OAAO,WAAW,UAAU,CAE7B,QAAO;;AAKX,SAAO;;AAIT,QAAO,UAAU,OAAO"}
1
+ {"version":3,"file":"formatAutoFilledFilePath.mjs","names":[],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"sourcesContent":["import { basename, dirname, isAbsolute, normalize, resolve } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport { GREY_DARK } from '@intlayer/config/colors';\nimport { colorize, colorizePath } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\n\nexport const formatAutoFilledFilePath = async (\n autoFillField: FilePathPattern,\n dictionaryKey: string,\n dictionaryFilePath: string,\n baseDir: string,\n configuration: IntlayerConfig,\n locale?: Locale\n): Promise<string> => {\n // Validate inputs\n if (!autoFillField) {\n throw new Error('autoFillField must be provided');\n }\n if (!dictionaryKey || typeof dictionaryKey !== 'string') {\n throw new Error('dictionaryKey must be a non-empty string');\n }\n if (!dictionaryFilePath || typeof dictionaryFilePath !== 'string') {\n throw new Error('dictionaryFilePath must be a non-empty string');\n }\n if (!baseDir || typeof baseDir !== 'string') {\n throw new Error('baseDir must be a non-empty string');\n }\n\n // Extract the original filename accurately\n const base = basename(dictionaryFilePath);\n\n const { fileExtensions } = configuration.content;\n const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));\n\n const originalFileName = extensionMatch\n ? base.slice(0, -extensionMatch.length)\n : base.split('.')[0];\n\n console.log({ extensionMatch });\n\n if (!extensionMatch) {\n throw new Error(\n `No extension found for file ${colorizePath(dictionaryFilePath)}. Valid extensions are: ${colorize(\n fileExtensions.join(', '),\n GREY_DARK\n )}`\n );\n }\n\n // Replace placeholders in autoFillField\n const result: string = await parseFilePathPattern(autoFillField, {\n key: dictionaryKey,\n fileName: originalFileName,\n locale,\n extension: extensionMatch,\n format: getFormatFromExtension(extensionMatch),\n });\n\n // Normalize the dictionary file path - if it's relative, make it absolute relative to baseDir\n const absoluteDictionaryPath = isAbsolute(dictionaryFilePath)\n ? dictionaryFilePath\n : resolve(baseDir, dictionaryFilePath);\n\n // Handle relative paths (starting with ./ or ../)\n if (result.startsWith('./') || result.startsWith('../')) {\n const fileDir = dirname(absoluteDictionaryPath);\n const resolvedPath = resolve(fileDir, result);\n\n return resolvedPath;\n }\n\n // Handle absolute paths\n if (isAbsolute(result)) {\n const normalizedResult = normalize(result);\n const normalizedBaseDir = normalize(baseDir);\n\n // Check if it's relative to baseDir (starts with /)\n // but not a system path (like /usr/local)\n if (\n result.startsWith('/') &&\n !normalizedResult.startsWith(normalizedBaseDir)\n ) {\n // Try to resolve it relative to baseDir first\n const relativeToBase = resolve(baseDir, result.substring(1));\n\n // If the path doesn't exist in common system directories, treat as relative to baseDir\n if (\n !result.startsWith('/usr/') &&\n !result.startsWith('/etc/') &&\n !result.startsWith('/var/') &&\n !result.startsWith('/home/') &&\n !result.startsWith('/Users/')\n ) {\n return relativeToBase;\n }\n }\n\n // It's a true system absolute path\n return normalizedResult;\n }\n\n // Default case: treat as relative to baseDir\n return normalize(result);\n};\n"],"mappings":";;;;;;;AASA,MAAa,2BAA2B,OACtC,eACA,eACA,oBACA,SACA,eACA,WACoB;AAEpB,KAAI,CAAC,cACH,OAAM,IAAI,MAAM,iCAAiC;AAEnD,KAAI,CAAC,iBAAiB,OAAO,kBAAkB,SAC7C,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,CAAC,sBAAsB,OAAO,uBAAuB,SACvD,OAAM,IAAI,MAAM,gDAAgD;AAElE,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,qCAAqC;CAIvD,MAAM,OAAO,SAAS,mBAAmB;CAEzC,MAAM,EAAE,mBAAmB,cAAc;CACzC,MAAM,iBAAiB,eAAe,MAAM,QAAQ,KAAK,SAAS,IAAI,CAAC;CAEvE,MAAM,mBAAmB,iBACrB,KAAK,MAAM,GAAG,CAAC,eAAe,OAAO,GACrC,KAAK,MAAM,IAAI,CAAC;AAEpB,SAAQ,IAAI,EAAE,gBAAgB,CAAC;AAE/B,KAAI,CAAC,eACH,OAAM,IAAI,MACR,+BAA+B,aAAa,mBAAmB,CAAC,0BAA0B,SACxF,eAAe,KAAK,KAAK,EACzB,UACD,GACF;CAIH,MAAM,SAAiB,MAAM,qBAAqB,eAAe;EAC/D,KAAK;EACL,UAAU;EACV;EACA,WAAW;EACX,QAAQ,uBAAuB,eAAe;EAC/C,CAAC;CAGF,MAAM,yBAAyB,WAAW,mBAAmB,GACzD,qBACA,QAAQ,SAAS,mBAAmB;AAGxC,KAAI,OAAO,WAAW,KAAK,IAAI,OAAO,WAAW,MAAM,CAIrD,QAFqB,QADL,QAAQ,uBACY,EAAE,OAEnB;AAIrB,KAAI,WAAW,OAAO,EAAE;EACtB,MAAM,mBAAmB,UAAU,OAAO;EAC1C,MAAM,oBAAoB,UAAU,QAAQ;AAI5C,MACE,OAAO,WAAW,IAAI,IACtB,CAAC,iBAAiB,WAAW,kBAAkB,EAC/C;GAEA,MAAM,iBAAiB,QAAQ,SAAS,OAAO,UAAU,EAAE,CAAC;AAG5D,OACE,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,QAAQ,IAC3B,CAAC,OAAO,WAAW,SAAS,IAC5B,CAAC,OAAO,WAAW,UAAU,CAE7B,QAAO;;AAKX,SAAO;;AAIT,QAAO,UAAU,OAAO"}
@@ -15,10 +15,14 @@ const groupByFilePath = (entries) => entries.reduce((acc, curr) => {
15
15
  const formatFillData = async (fillField, localeList, filePath, dictionaryKey, configuration) => {
16
16
  if (!fillField || typeof fillField === "boolean") return [];
17
17
  const { baseDir } = configuration.system;
18
- const { defaultLocale } = configuration.internationalization;
18
+ const { defaultLocale, locales } = configuration.internationalization;
19
19
  const extension = extname(filePath);
20
20
  const base = basename(filePath);
21
- const cleanComponentFileName = base.includes(".content.") ? base.split(".content.")[0] : base.split(".")[0];
21
+ const { fileExtensions } = configuration.content;
22
+ const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));
23
+ let cleanComponentFileName = extensionMatch ? base.slice(0, -extensionMatch.length) : base.split(".")[0];
24
+ const sourceLocaleMatch = locales.find((loc) => cleanComponentFileName.endsWith(`.${loc}`));
25
+ if (sourceLocaleMatch) cleanComponentFileName = cleanComponentFileName.slice(0, -(sourceLocaleMatch.length + 1));
22
26
  const uncapitalizedName = cleanComponentFileName.charAt(0).toLowerCase() + cleanComponentFileName.slice(1);
23
27
  const componentFormat = getFormatFromExtension(extension);
24
28
  const getContext = (locale, patternString) => {
@@ -36,7 +40,7 @@ const formatFillData = async (fillField, localeList, filePath, dictionaryKey, co
36
40
  componentExtension: extension,
37
41
  format,
38
42
  locale,
39
- extension: configuration.content.fileExtensions[0]
43
+ extension: configuration.content.fileExtensions.find((ext) => ext.endsWith(extension)) ?? configuration.content.fileExtensions[0]
40
44
  };
41
45
  };
42
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"formatFillData.mjs","names":[],"sources":["../../../src/fill/formatFillData.ts"],"sourcesContent":["import { basename, dirname, extname, relative } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DictionaryKey, Fill } from '@intlayer/types/dictionary';\nimport type { FilePathPatternContext } from '@intlayer/types/filePathPattern';\nimport {\n getPatternForLocale,\n resolveOutputPattern,\n} from '../utils/getOutputFilePath';\n\nexport type FillData = {\n localeList: Locale[];\n filePath: string;\n isPerLocale: boolean;\n};\n\n/** Merge FillData entries that resolve to the same file path. */\nconst groupByFilePath = (entries: FillData[]): FillData[] =>\n entries.reduce((acc, curr) => {\n const existing = acc.find((item) => item.filePath === curr.filePath);\n if (existing) {\n for (const loc of curr.localeList) {\n if (!existing.localeList.includes(loc)) existing.localeList.push(loc);\n }\n // Multiple locales sharing one path → multilingual file\n existing.isPerLocale = false;\n } else {\n acc.push(curr);\n }\n return acc;\n }, [] as FillData[]);\n\nexport const formatFillData = async (\n fillField: Fill,\n localeList: Locale[],\n filePath: string,\n dictionaryKey: DictionaryKey,\n configuration: IntlayerConfig\n): Promise<FillData[]> => {\n if (!fillField || typeof fillField === 'boolean') return [];\n\n const { baseDir } = configuration.system;\n const { defaultLocale } = configuration.internationalization;\n\n const extension = extname(filePath);\n const base = basename(filePath);\n const cleanComponentFileName = base.includes('.content.')\n ? base.split('.content.')[0]\n : base.split('.')[0];\n const uncapitalizedName =\n cleanComponentFileName.charAt(0).toLowerCase() +\n cleanComponentFileName.slice(1);\n const componentFormat = getFormatFromExtension(extension);\n\n const getContext = (\n locale: Locale,\n patternString?: string\n ): FilePathPatternContext => {\n let format: FilePathPatternContext['format'] = 'json';\n if (patternString) {\n const extFormat = getFormatFromExtension(extname(patternString) as any);\n if (extFormat) format = extFormat as any;\n }\n return {\n key: dictionaryKey,\n componentDirPath: relative(baseDir, dirname(filePath)),\n componentFileName: cleanComponentFileName,\n fileName: uncapitalizedName,\n componentFormat:\n componentFormat as FilePathPatternContext['componentFormat'],\n componentExtension:\n extension as FilePathPatternContext['componentExtension'],\n format,\n locale,\n extension: configuration.content.fileExtensions[0],\n };\n };\n\n /**\n * Evaluate a scalar Fill pattern (string or function) for a list of locales.\n *\n * - Uses a dummy locale probe to detect whether the pattern varies per locale.\n * - `forcePerLocale` overrides the probe for object-fill entries where each\n * locale always maps to its own dedicated file regardless of whether the\n * path itself contains the locale string.\n */\n const processPattern = async (\n pattern: Fill,\n locales: Locale[],\n forcePerLocale = false\n ): Promise<FillData[]> => {\n const dummyLocale = '###########locale###########' as Locale;\n const patternString = typeof pattern === 'string' ? pattern : undefined;\n\n const dummyPath = await resolveOutputPattern(\n pattern,\n dummyLocale,\n getContext(dummyLocale, patternString),\n filePath,\n baseDir\n );\n const isPatternPerLocale =\n forcePerLocale ||\n (typeof dummyPath === 'string' && dummyPath.includes(dummyLocale));\n\n if (isPatternPerLocale) {\n const resolvedPaths: FillData[] = [];\n for (const locale of locales) {\n const absolutePath = await resolveOutputPattern(\n pattern,\n locale,\n getContext(locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath !== false) {\n resolvedPaths.push({\n filePath: absolutePath,\n localeList: [locale],\n isPerLocale: true,\n });\n }\n }\n return groupByFilePath(resolvedPaths);\n }\n\n // Single multilingual path — resolve using the default locale for context\n const absolutePath = await resolveOutputPattern(\n pattern,\n defaultLocale as Locale,\n getContext(defaultLocale as Locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath === false) return [];\n return [\n { filePath: absolutePath, localeList: locales, isPerLocale: false },\n ];\n };\n\n // Object fill: each locale key maps to its own pattern.\n // Object fill entries are always per-locale in intent (each locale → its own file).\n if (typeof fillField === 'object' && fillField !== null) {\n const results: FillData[] = [];\n for (const locale of localeList) {\n const pattern = getPatternForLocale(fillField, locale);\n if (pattern) {\n const res = await processPattern(pattern, [locale], true);\n results.push(...res);\n }\n }\n return groupByFilePath(results);\n }\n\n // Scalar string or function pattern\n return processPattern(fillField, localeList);\n};\n"],"mappings":";;;;;;AAkBA,MAAM,mBAAmB,YACvB,QAAQ,QAAQ,KAAK,SAAS;CAC5B,MAAM,WAAW,IAAI,MAAM,SAAS,KAAK,aAAa,KAAK,SAAS;AACpE,KAAI,UAAU;AACZ,OAAK,MAAM,OAAO,KAAK,WACrB,KAAI,CAAC,SAAS,WAAW,SAAS,IAAI,CAAE,UAAS,WAAW,KAAK,IAAI;AAGvE,WAAS,cAAc;OAEvB,KAAI,KAAK,KAAK;AAEhB,QAAO;GACN,EAAE,CAAe;AAEtB,MAAa,iBAAiB,OAC5B,WACA,YACA,UACA,eACA,kBACwB;AACxB,KAAI,CAAC,aAAa,OAAO,cAAc,UAAW,QAAO,EAAE;CAE3D,MAAM,EAAE,YAAY,cAAc;CAClC,MAAM,EAAE,kBAAkB,cAAc;CAExC,MAAM,YAAY,QAAQ,SAAS;CACnC,MAAM,OAAO,SAAS,SAAS;CAC/B,MAAM,yBAAyB,KAAK,SAAS,YAAY,GACrD,KAAK,MAAM,YAAY,CAAC,KACxB,KAAK,MAAM,IAAI,CAAC;CACpB,MAAM,oBACJ,uBAAuB,OAAO,EAAE,CAAC,aAAa,GAC9C,uBAAuB,MAAM,EAAE;CACjC,MAAM,kBAAkB,uBAAuB,UAAU;CAEzD,MAAM,cACJ,QACA,kBAC2B;EAC3B,IAAI,SAA2C;AAC/C,MAAI,eAAe;GACjB,MAAM,YAAY,uBAAuB,QAAQ,cAAc,CAAQ;AACvE,OAAI,UAAW,UAAS;;AAE1B,SAAO;GACL,KAAK;GACL,kBAAkB,SAAS,SAAS,QAAQ,SAAS,CAAC;GACtD,mBAAmB;GACnB,UAAU;GAER;GACF,oBACE;GACF;GACA;GACA,WAAW,cAAc,QAAQ,eAAe;GACjD;;;;;;;;;;CAWH,MAAM,iBAAiB,OACrB,SACA,SACA,iBAAiB,UACO;EACxB,MAAM,cAAc;EACpB,MAAM,gBAAgB,OAAO,YAAY,WAAW,UAAU;EAE9D,MAAM,YAAY,MAAM,qBACtB,SACA,aACA,WAAW,aAAa,cAAc,EACtC,UACA,QACD;AAKD,MAHE,kBACC,OAAO,cAAc,YAAY,UAAU,SAAS,YAAY,EAE3C;GACtB,MAAM,gBAA4B,EAAE;AACpC,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,eAAe,MAAM,qBACzB,SACA,QACA,WAAW,QAAQ,cAAc,EACjC,UACA,QACD;AACD,QAAI,iBAAiB,MACnB,eAAc,KAAK;KACjB,UAAU;KACV,YAAY,CAAC,OAAO;KACpB,aAAa;KACd,CAAC;;AAGN,UAAO,gBAAgB,cAAc;;EAIvC,MAAM,eAAe,MAAM,qBACzB,SACA,eACA,WAAW,eAAyB,cAAc,EAClD,UACA,QACD;AACD,MAAI,iBAAiB,MAAO,QAAO,EAAE;AACrC,SAAO,CACL;GAAE,UAAU;GAAc,YAAY;GAAS,aAAa;GAAO,CACpE;;AAKH,KAAI,OAAO,cAAc,YAAY,cAAc,MAAM;EACvD,MAAM,UAAsB,EAAE;AAC9B,OAAK,MAAM,UAAU,YAAY;GAC/B,MAAM,UAAU,oBAAoB,WAAW,OAAO;AACtD,OAAI,SAAS;IACX,MAAM,MAAM,MAAM,eAAe,SAAS,CAAC,OAAO,EAAE,KAAK;AACzD,YAAQ,KAAK,GAAG,IAAI;;;AAGxB,SAAO,gBAAgB,QAAQ;;AAIjC,QAAO,eAAe,WAAW,WAAW"}
1
+ {"version":3,"file":"formatFillData.mjs","names":[],"sources":["../../../src/fill/formatFillData.ts"],"sourcesContent":["import { basename, dirname, extname, relative } from 'node:path';\nimport { getFormatFromExtension } from '@intlayer/chokidar/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DictionaryKey, Fill } from '@intlayer/types/dictionary';\nimport type { FilePathPatternContext } from '@intlayer/types/filePathPattern';\nimport {\n getPatternForLocale,\n resolveOutputPattern,\n} from '../utils/getOutputFilePath';\n\nexport type FillData = {\n localeList: Locale[];\n filePath: string;\n isPerLocale: boolean;\n};\n\n/** Merge FillData entries that resolve to the same file path. */\nconst groupByFilePath = (entries: FillData[]): FillData[] =>\n entries.reduce((acc, curr) => {\n const existing = acc.find((item) => item.filePath === curr.filePath);\n if (existing) {\n for (const loc of curr.localeList) {\n if (!existing.localeList.includes(loc)) existing.localeList.push(loc);\n }\n // Multiple locales sharing one path → multilingual file\n existing.isPerLocale = false;\n } else {\n acc.push(curr);\n }\n return acc;\n }, [] as FillData[]);\n\nexport const formatFillData = async (\n fillField: Fill,\n localeList: Locale[],\n filePath: string,\n dictionaryKey: DictionaryKey,\n configuration: IntlayerConfig\n): Promise<FillData[]> => {\n if (!fillField || typeof fillField === 'boolean') return [];\n\n const { baseDir } = configuration.system;\n const { defaultLocale, locales } = configuration.internationalization;\n\n const extension = extname(filePath);\n const base = basename(filePath);\n\n const { fileExtensions } = configuration.content;\n const extensionMatch = fileExtensions.find((ext) => base.endsWith(ext));\n\n let cleanComponentFileName = extensionMatch\n ? base.slice(0, -extensionMatch.length)\n : base.split('.')[0];\n\n // Strip source locale if present\n const sourceLocaleMatch = locales.find((loc) =>\n cleanComponentFileName.endsWith(`.${loc}`)\n );\n if (sourceLocaleMatch) {\n cleanComponentFileName = cleanComponentFileName.slice(\n 0,\n -(sourceLocaleMatch.length + 1)\n );\n }\n\n const uncapitalizedName =\n cleanComponentFileName.charAt(0).toLowerCase() +\n cleanComponentFileName.slice(1);\n const componentFormat = getFormatFromExtension(extension);\n\n const getContext = (\n locale: Locale,\n patternString?: string\n ): FilePathPatternContext => {\n let format: FilePathPatternContext['format'] = 'json';\n if (patternString) {\n const extFormat = getFormatFromExtension(extname(patternString) as any);\n if (extFormat) format = extFormat as any;\n }\n return {\n key: dictionaryKey,\n componentDirPath: relative(baseDir, dirname(filePath)),\n componentFileName: cleanComponentFileName,\n fileName: uncapitalizedName,\n componentFormat:\n componentFormat as FilePathPatternContext['componentFormat'],\n componentExtension:\n extension as FilePathPatternContext['componentExtension'],\n format,\n locale,\n extension:\n configuration.content.fileExtensions.find((ext) =>\n ext.endsWith(extension)\n ) ?? configuration.content.fileExtensions[0],\n };\n };\n\n /**\n * Evaluate a scalar Fill pattern (string or function) for a list of locales.\n *\n * - Uses a dummy locale probe to detect whether the pattern varies per locale.\n * - `forcePerLocale` overrides the probe for object-fill entries where each\n * locale always maps to its own dedicated file regardless of whether the\n * path itself contains the locale string.\n */\n const processPattern = async (\n pattern: Fill,\n locales: Locale[],\n forcePerLocale = false\n ): Promise<FillData[]> => {\n const dummyLocale = '###########locale###########' as Locale;\n const patternString = typeof pattern === 'string' ? pattern : undefined;\n\n const dummyPath = await resolveOutputPattern(\n pattern,\n dummyLocale,\n getContext(dummyLocale, patternString),\n filePath,\n baseDir\n );\n const isPatternPerLocale =\n forcePerLocale ||\n (typeof dummyPath === 'string' && dummyPath.includes(dummyLocale));\n\n if (isPatternPerLocale) {\n const resolvedPaths: FillData[] = [];\n for (const locale of locales) {\n const absolutePath = await resolveOutputPattern(\n pattern,\n locale,\n getContext(locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath !== false) {\n resolvedPaths.push({\n filePath: absolutePath,\n localeList: [locale],\n isPerLocale: true,\n });\n }\n }\n return groupByFilePath(resolvedPaths);\n }\n\n // Single multilingual path — resolve using the default locale for context\n const absolutePath = await resolveOutputPattern(\n pattern,\n defaultLocale as Locale,\n getContext(defaultLocale as Locale, patternString),\n filePath,\n baseDir\n );\n if (absolutePath === false) return [];\n return [\n { filePath: absolutePath, localeList: locales, isPerLocale: false },\n ];\n };\n\n // Object fill: each locale key maps to its own pattern.\n // Object fill entries are always per-locale in intent (each locale → its own file).\n if (typeof fillField === 'object' && fillField !== null) {\n const results: FillData[] = [];\n for (const locale of localeList) {\n const pattern = getPatternForLocale(fillField, locale);\n if (pattern) {\n const res = await processPattern(pattern, [locale], true);\n results.push(...res);\n }\n }\n return groupByFilePath(results);\n }\n\n // Scalar string or function pattern\n return processPattern(fillField, localeList);\n};\n"],"mappings":"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"}
@@ -29,7 +29,7 @@ const serializeError = (error) => {
29
29
  return String(error);
30
30
  }
31
31
  };
32
- const CHUNK_SIZE = 1500;
32
+ const CHUNK_SIZE = 4e3;
33
33
  const GROUP_MAX_RETRY = 2;
34
34
  const MAX_RETRY = 3;
35
35
  const RETRY_DELAY = 1e3 * 10;
@@ -86,11 +86,13 @@ const translateDictionary = async (task, configuration, options) => {
86
86
  let dictionaryToProcess = structuredClone(baseUnmergedDictionary);
87
87
  let targetLocaleDictionary;
88
88
  if (typeof baseUnmergedDictionary.locale === "string") {
89
- const targetLocaleFilePath = baseUnmergedDictionary.filePath?.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`);
90
- targetLocaleDictionary = (targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0) ?? {
89
+ const targetUnmergedDictionary = unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.locale === targetLocale && dict.filePath !== baseUnmergedDictionary.filePath);
90
+ const sourceFilePath = baseUnmergedDictionary.filePath ?? "";
91
+ const derivedTargetFilePath = sourceFilePath.includes(`/${task.sourceLocale}/`) ? sourceFilePath.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`) : sourceFilePath.replace(new RegExp(`(^|/)${task.sourceLocale}(\\.[^/]+)$`), `$1${targetLocale}$2`);
92
+ targetLocaleDictionary = targetUnmergedDictionary ?? {
91
93
  key: baseUnmergedDictionary.key,
92
94
  content: {},
93
- filePath: targetLocaleFilePath,
95
+ filePath: derivedTargetFilePath || void 0,
94
96
  locale: targetLocale
95
97
  };
96
98
  } else {
@@ -1 +1 @@
1
- {"version":3,"file":"translateDictionary.mjs","names":[],"sources":["../../../src/fill/translateDictionary.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport type { AIConfig } from '@intlayer/ai';\nimport { type AIOptions, getIntlayerAPIProxy } from '@intlayer/api';\nimport {\n chunkJSON,\n excludeObjectFormat,\n formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n verifyIdenticObjectFormat,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n colorizePath,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { retryManager } from '@intlayer/config/utils';\nimport {\n getFilterMissingTranslationsDictionary,\n getMultilingualDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport type { AIClient } from '../utils/setupAI';\nimport { deepMergeContent } from './deepMergeContent';\nimport {\n extractTranslatableContent,\n reinsertTranslatedContent,\n} from './extractTranslatableContent';\nimport type { TranslationTask } from './listTranslationsTasks';\n\ntype TranslateDictionaryResult = TranslationTask & {\n dictionaryOutput: Dictionary | null;\n};\n\ntype TranslateDictionaryOptions = {\n mode: 'complete' | 'review';\n aiOptions?: AIOptions;\n fillMetadata?: boolean;\n onHandle?: ReturnType<\n typeof import('@intlayer/chokidar/utils').getGlobalLimiter\n >;\n onSuccess?: () => void;\n onError?: (error: unknown) => void;\n getAbortError?: () => Error | null;\n aiClient?: AIClient;\n aiConfig?: AIConfig;\n};\n\nconst createChunkPreset = (chunkIndex: number, totalChunks: number) => {\n if (totalChunks <= 1) return '';\n return colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n colorizeNumber(chunkIndex + 1),\n colorize(`/${totalChunks}`, ANSIColors.GREY_DARK),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 5 }\n );\n};\n\nconst hasMissingMetadata = (dictionary: Dictionary) =>\n !dictionary.description || !dictionary.title || !dictionary.tags;\n\nconst serializeError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.cause\n ? `${error.message} (cause: ${String(error.cause)})`\n : error.message;\n }\n if (typeof error === 'string') return error;\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n};\n\nconst CHUNK_SIZE = 1500; // Smaller chunks for better accuracy and structural integrity\nconst GROUP_MAX_RETRY = 2;\nconst MAX_RETRY = 3;\nconst RETRY_DELAY = 1000 * 10; // 10 seconds\n\nconst MAX_FOLLOWING_ERRORS = 10; // 10 errors in a row, hard exit the process\nlet followingErrors = 0;\n\nexport const translateDictionary = async (\n task: TranslationTask,\n configuration: IntlayerConfig,\n options?: TranslateDictionaryOptions\n): Promise<TranslateDictionaryResult> => {\n const appLogger = getAppLogger(configuration);\n const intlayerAPI = getIntlayerAPIProxy(undefined, configuration);\n\n const { mode, aiOptions, fillMetadata, aiClient, aiConfig } = {\n mode: 'complete',\n fillMetadata: true,\n ...options,\n } as const;\n\n const notifySuccess = () => {\n followingErrors = 0;\n options?.onSuccess?.();\n };\n\n const result = await retryManager(\n async () => {\n const unmergedDictionariesRecord = getUnmergedDictionaries(configuration);\n\n const baseUnmergedDictionary: Dictionary | undefined =\n unmergedDictionariesRecord[task.dictionaryKey].find(\n (dict) => dict.localId === task.dictionaryLocalId\n );\n\n if (!baseUnmergedDictionary) {\n appLogger(\n `${task.dictionaryPreset}Dictionary not found in unmergedDictionariesRecord. Skipping.`,\n {\n level: 'warn',\n }\n );\n return { ...task, dictionaryOutput: null };\n }\n\n let metadata:\n | Pick<Dictionary, 'description' | 'title' | 'tags'>\n | undefined;\n\n if (\n fillMetadata &&\n (hasMissingMetadata(baseUnmergedDictionary) || mode === 'review')\n ) {\n const defaultLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n configuration.internationalization.defaultLocale\n );\n\n appLogger(\n `${task.dictionaryPreset} Filling missing metadata for ${colorizePath(basename(baseUnmergedDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const runAudit = async () => {\n if (aiClient && aiConfig) {\n const result = await aiClient.auditDictionaryMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiConfig,\n });\n\n return {\n data: result,\n };\n }\n\n return await intlayerAPI.ai.auditContentDeclarationMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiOptions,\n });\n };\n\n const metadataResult = options?.onHandle\n ? await options.onHandle(runAudit)\n : await runAudit();\n\n metadata = metadataResult.data?.fileContent;\n }\n\n const translatedContentResults = await Promise.all(\n task.targetLocales.map(async (targetLocale) => {\n /**\n * In complete mode, for large dictionaries, we want to filter all content that is already translated\n *\n * targetLocale: fr\n *\n * { test1: t({ ar: 'Hello', en: 'Hello', fr: 'Bonjour' } }) -> {}\n * { test2: t({ ar: 'Hello', en: 'Hello' }) } -> { test2: t({ ar: 'Hello', en: 'Hello' }) }\n *\n */\n // Reset to base dictionary for each locale to ensure we filter from the original\n let dictionaryToProcess = structuredClone(baseUnmergedDictionary);\n\n let targetLocaleDictionary: Dictionary;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // For per-locale files, the content is already in simple JSON format (not translation nodes)\n // The base dictionary is already the source locale content\n\n // Load the existing target locale dictionary\n const targetLocaleFilePath =\n baseUnmergedDictionary.filePath?.replace(\n new RegExp(`/${task.sourceLocale}/`, 'g'),\n `/${targetLocale}/`\n );\n\n // Find the target locale dictionary in unmerged dictionaries\n const targetUnmergedDictionary = targetLocaleFilePath\n ? unmergedDictionariesRecord[task.dictionaryKey]?.find(\n (dict) =>\n dict.filePath === targetLocaleFilePath &&\n dict.locale === targetLocale\n )\n : undefined;\n\n targetLocaleDictionary = targetUnmergedDictionary ?? {\n key: baseUnmergedDictionary.key,\n content: {},\n filePath: targetLocaleFilePath,\n locale: targetLocale,\n };\n } else {\n // For multilingual dictionaries\n if (mode === 'complete') {\n // Remove all nodes that don't have any content to translate\n dictionaryToProcess = getFilterMissingTranslationsDictionary(\n dictionaryToProcess,\n targetLocale\n );\n }\n\n dictionaryToProcess = getPerLocaleDictionary(\n dictionaryToProcess,\n task.sourceLocale\n );\n\n targetLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n targetLocale\n );\n }\n\n // Filter to only untranslated fields, preserving explicit null values as\n // default-locale fallback markers. Applied after both paths converge so\n // the same logic covers per-locale and multilingual dictionaries.\n if (mode === 'complete') {\n dictionaryToProcess = {\n ...dictionaryToProcess,\n content:\n excludeObjectFormat(\n dictionaryToProcess.content,\n targetLocaleDictionary.content\n ) ?? {},\n };\n }\n\n const localePreset = colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n formatLocale(targetLocale),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 18 }\n );\n\n appLogger(\n `${task.dictionaryPreset}${localePreset} Preparing ${colorizePath(basename(targetLocaleDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n dictionaryToProcess.content as unknown as Record<string, any>,\n CHUNK_SIZE\n );\n\n const nbOfChunks = chunkedJsonContent.length;\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset} Split into ${colorizeNumber(nbOfChunks)} chunks for translation`,\n {\n level: 'info',\n }\n );\n }\n\n const chunkResult: JsonChunk[] = [];\n\n // Process chunks in parallel (globally throttled) to allow concurrent translation\n const chunkPromises = chunkedJsonContent.map(async (chunk) => {\n const chunkPreset = createChunkPreset(chunk.index, chunk.total);\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`,\n {\n level: 'info',\n }\n );\n }\n\n const reconstructedChunk = reconstructFromSingleChunk(chunk);\n const {\n extractedContent: chunkExtractedContent,\n translatableDictionary: chunkTranslatableDictionary,\n } = extractTranslatableContent(reconstructedChunk);\n\n const executeTranslation = async () => {\n return await retryManager(\n async () => {\n let translationResult: any;\n\n if (aiClient && aiConfig) {\n translationResult = await aiClient.translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiConfig,\n });\n } else {\n translationResult = await intlayerAPI.ai\n .translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiOptions,\n })\n .then((result) => result.data);\n }\n\n if (!translationResult?.fileContent) {\n throw new Error('No content result');\n }\n\n const { isIdentic, error } = verifyIdenticObjectFormat(\n translationResult.fileContent,\n chunkTranslatableDictionary\n );\n\n if (!isIdentic) {\n throw new Error(\n `Translation result does not match expected format: ${error}`\n );\n }\n\n notifySuccess();\n return reinsertTranslatedContent(\n reconstructedChunk,\n chunkExtractedContent,\n translationResult.fileContent as Record<number, string>\n );\n },\n {\n maxRetry: MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) => {\n const chunkPreset = createChunkPreset(\n chunk.index,\n chunk.total\n );\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} ${colorize('Error filling:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n );\n\n followingErrors += 1;\n\n if (followingErrors >= MAX_FOLLOWING_ERRORS) {\n appLogger(`There is something wrong.`, {\n level: 'error',\n });\n process.exit(1); // 1 for error\n }\n },\n }\n )();\n };\n\n const wrapped = options?.onHandle\n ? options.onHandle(executeTranslation) // queued in global limiter\n : executeTranslation(); // no global limiter\n\n return wrapped.then((result) => ({ chunk, result }));\n });\n\n // Wait for all chunks for this locale in parallel (still capped by global limiter)\n const chunkResults = await Promise.all(chunkPromises);\n\n // Maintain order\n chunkResults\n .sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index)\n .forEach(({ result }) => {\n chunkResult.push(result);\n });\n\n // Merge translated chunk contents back into a single content object\n const reinsertedContent = mergeChunks(chunkResult);\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // Merge newly translated content (including explicit null fallbacks) back\n // into the existing target locale content. Applies to both per-locale and\n // multilingual paths so the target always retains previously translated\n // fields and receives null markers where the source has no translation.\n const finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n merged.content\n );\n\n return [targetLocale, finalContent] as const;\n })\n );\n\n const translatedContent: Partial<Record<Locale, Dictionary['content']>> =\n Object.fromEntries(translatedContentResults);\n\n const baseDictionary = baseUnmergedDictionary.locale\n ? {\n ...baseUnmergedDictionary,\n key: baseUnmergedDictionary.key!,\n content: {},\n }\n : baseUnmergedDictionary;\n\n let dictionaryOutput: Dictionary = {\n ...getMultilingualDictionary(baseDictionary),\n locale: undefined, // Ensure the dictionary is multilingual\n ...metadata,\n };\n\n for (const targetLocale of task.targetLocales) {\n if (translatedContent[targetLocale]) {\n dictionaryOutput = insertContentInDictionary(\n dictionaryOutput,\n translatedContent[targetLocale],\n targetLocale\n );\n }\n }\n\n appLogger(\n `${task.dictionaryPreset} ${colorize('Translation completed successfully', ANSIColors.GREEN)} for ${colorizePath(basename(dictionaryOutput.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n // The dict-level `fill` value may have been lost during JSON serialisation\n // (functions can't be serialised to JSON). Fall back to the config-level\n // fill so that an explicit fill in intlayer.config.ts is honoured.\n const effectiveFillForCheck =\n baseUnmergedDictionary.fill ?? configuration.dictionary?.fill;\n\n if (\n baseUnmergedDictionary.locale &&\n (effectiveFillForCheck === true ||\n effectiveFillForCheck === undefined) &&\n baseUnmergedDictionary.location === 'local'\n ) {\n const dictionaryFilePath = baseUnmergedDictionary\n .filePath!.split('.')\n .slice(0, -1);\n\n const contentIndex = dictionaryFilePath[dictionaryFilePath.length - 1];\n\n return JSON.parse(\n JSON.stringify({\n ...task,\n dictionaryOutput: {\n ...dictionaryOutput,\n fill: undefined,\n filled: true,\n },\n }).replaceAll(\n new RegExp(`\\\\.${contentIndex}\\\\.[a-zA-Z0-9]+`, 'g'),\n `.filled.${contentIndex}.json`\n )\n );\n }\n\n return {\n ...task,\n dictionaryOutput,\n };\n },\n {\n maxRetry: GROUP_MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Error:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n ),\n onMaxTryReached: ({ error }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Maximum number of retries reached:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)}`,\n {\n level: 'error',\n }\n ),\n }\n )();\n\n return result as TranslateDictionaryResult;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"translateDictionary.mjs","names":[],"sources":["../../../src/fill/translateDictionary.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport type { AIConfig } from '@intlayer/ai';\nimport { type AIOptions, getIntlayerAPIProxy } from '@intlayer/api';\nimport {\n chunkJSON,\n excludeObjectFormat,\n formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n verifyIdenticObjectFormat,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n colorizePath,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { retryManager } from '@intlayer/config/utils';\nimport {\n getFilterMissingTranslationsDictionary,\n getMultilingualDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport type { AIClient } from '../utils/setupAI';\nimport { deepMergeContent } from './deepMergeContent';\nimport {\n extractTranslatableContent,\n reinsertTranslatedContent,\n} from './extractTranslatableContent';\nimport type { TranslationTask } from './listTranslationsTasks';\n\ntype TranslateDictionaryResult = TranslationTask & {\n dictionaryOutput: Dictionary | null;\n};\n\ntype TranslateDictionaryOptions = {\n mode: 'complete' | 'review';\n aiOptions?: AIOptions;\n fillMetadata?: boolean;\n onHandle?: ReturnType<\n typeof import('@intlayer/chokidar/utils').getGlobalLimiter\n >;\n onSuccess?: () => void;\n onError?: (error: unknown) => void;\n getAbortError?: () => Error | null;\n aiClient?: AIClient;\n aiConfig?: AIConfig;\n};\n\nconst createChunkPreset = (chunkIndex: number, totalChunks: number) => {\n if (totalChunks <= 1) return '';\n return colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n colorizeNumber(chunkIndex + 1),\n colorize(`/${totalChunks}`, ANSIColors.GREY_DARK),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 5 }\n );\n};\n\nconst hasMissingMetadata = (dictionary: Dictionary) =>\n !dictionary.description || !dictionary.title || !dictionary.tags;\n\nconst serializeError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.cause\n ? `${error.message} (cause: ${String(error.cause)})`\n : error.message;\n }\n if (typeof error === 'string') return error;\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n};\n\nconst CHUNK_SIZE = 4000;\nconst GROUP_MAX_RETRY = 2;\nconst MAX_RETRY = 3;\nconst RETRY_DELAY = 1000 * 10; // 10 seconds\n\nconst MAX_FOLLOWING_ERRORS = 10; // 10 errors in a row, hard exit the process\nlet followingErrors = 0;\n\nexport const translateDictionary = async (\n task: TranslationTask,\n configuration: IntlayerConfig,\n options?: TranslateDictionaryOptions\n): Promise<TranslateDictionaryResult> => {\n const appLogger = getAppLogger(configuration);\n const intlayerAPI = getIntlayerAPIProxy(undefined, configuration);\n\n const { mode, aiOptions, fillMetadata, aiClient, aiConfig } = {\n mode: 'complete',\n fillMetadata: true,\n ...options,\n } as const;\n\n const notifySuccess = () => {\n followingErrors = 0;\n options?.onSuccess?.();\n };\n\n const result = await retryManager(\n async () => {\n const unmergedDictionariesRecord = getUnmergedDictionaries(configuration);\n\n const baseUnmergedDictionary: Dictionary | undefined =\n unmergedDictionariesRecord[task.dictionaryKey].find(\n (dict) => dict.localId === task.dictionaryLocalId\n );\n\n if (!baseUnmergedDictionary) {\n appLogger(\n `${task.dictionaryPreset}Dictionary not found in unmergedDictionariesRecord. Skipping.`,\n {\n level: 'warn',\n }\n );\n return { ...task, dictionaryOutput: null };\n }\n\n let metadata:\n | Pick<Dictionary, 'description' | 'title' | 'tags'>\n | undefined;\n\n if (\n fillMetadata &&\n (hasMissingMetadata(baseUnmergedDictionary) || mode === 'review')\n ) {\n const defaultLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n configuration.internationalization.defaultLocale\n );\n\n appLogger(\n `${task.dictionaryPreset} Filling missing metadata for ${colorizePath(basename(baseUnmergedDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const runAudit = async () => {\n if (aiClient && aiConfig) {\n const result = await aiClient.auditDictionaryMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiConfig,\n });\n\n return {\n data: result,\n };\n }\n\n return await intlayerAPI.ai.auditContentDeclarationMetadata({\n fileContent: JSON.stringify(defaultLocaleDictionary),\n aiOptions,\n });\n };\n\n const metadataResult = options?.onHandle\n ? await options.onHandle(runAudit)\n : await runAudit();\n\n metadata = metadataResult.data?.fileContent;\n }\n\n const translatedContentResults = await Promise.all(\n task.targetLocales.map(async (targetLocale) => {\n /**\n * In complete mode, for large dictionaries, we want to filter all content that is already translated\n *\n * targetLocale: fr\n *\n * { test1: t({ ar: 'Hello', en: 'Hello', fr: 'Bonjour' } }) -> {}\n * { test2: t({ ar: 'Hello', en: 'Hello' }) } -> { test2: t({ ar: 'Hello', en: 'Hello' }) }\n *\n */\n // Reset to base dictionary for each locale to ensure we filter from the original\n let dictionaryToProcess = structuredClone(baseUnmergedDictionary);\n\n let targetLocaleDictionary: Dictionary;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // For per-locale files, the content is already in simple JSON format (not translation nodes)\n // The base dictionary is already the source locale content\n\n // Look up the target locale dictionary by its locale property.\n // This handles both directory-based paths (/en/file.json → /es/file.json)\n // AND filename-based paths (messages_ICU/en.json → messages_ICU/es.json).\n // Plugins like syncJSON set `dict.locale` correctly, so matching by locale\n // is the only reliable strategy — path string-replacement fails when the\n // locale appears in the filename rather than a directory segment.\n const targetUnmergedDictionary = unmergedDictionariesRecord[\n task.dictionaryKey\n ]?.find(\n (dict) =>\n dict.locale === targetLocale &&\n dict.filePath !== baseUnmergedDictionary.filePath\n );\n\n // Derive the target file path for the fallback (when the file doesn't exist yet).\n // Try directory-style first (/en/ → /es/), then filename-style (en.json → es.json).\n const sourceFilePath = baseUnmergedDictionary.filePath ?? '';\n const derivedTargetFilePath = sourceFilePath.includes(\n `/${task.sourceLocale}/`\n )\n ? sourceFilePath.replace(\n new RegExp(`/${task.sourceLocale}/`, 'g'),\n `/${targetLocale}/`\n )\n : sourceFilePath.replace(\n new RegExp(`(^|/)${task.sourceLocale}(\\\\.[^/]+)$`),\n `$1${targetLocale}$2`\n );\n\n targetLocaleDictionary = targetUnmergedDictionary ?? {\n key: baseUnmergedDictionary.key,\n content: {},\n filePath: derivedTargetFilePath || undefined,\n locale: targetLocale,\n };\n } else {\n // For multilingual dictionaries\n if (mode === 'complete') {\n // Remove all nodes that don't have any content to translate\n dictionaryToProcess = getFilterMissingTranslationsDictionary(\n dictionaryToProcess,\n targetLocale\n );\n }\n\n dictionaryToProcess = getPerLocaleDictionary(\n dictionaryToProcess,\n task.sourceLocale\n );\n\n targetLocaleDictionary = getPerLocaleDictionary(\n baseUnmergedDictionary,\n targetLocale\n );\n }\n\n // Filter to only untranslated fields, preserving explicit null values as\n // default-locale fallback markers. Applied after both paths converge so\n // the same logic covers per-locale and multilingual dictionaries.\n if (mode === 'complete') {\n dictionaryToProcess = {\n ...dictionaryToProcess,\n content:\n excludeObjectFormat(\n dictionaryToProcess.content,\n targetLocaleDictionary.content\n ) ?? {},\n };\n }\n\n const localePreset = colon(\n [\n colorize('[', ANSIColors.GREY_DARK),\n formatLocale(targetLocale),\n colorize(']', ANSIColors.GREY_DARK),\n ].join(''),\n { colSize: 18 }\n );\n\n appLogger(\n `${task.dictionaryPreset}${localePreset} Preparing ${colorizePath(basename(targetLocaleDictionary.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n dictionaryToProcess.content as unknown as Record<string, any>,\n CHUNK_SIZE\n );\n\n const nbOfChunks = chunkedJsonContent.length;\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset} Split into ${colorizeNumber(nbOfChunks)} chunks for translation`,\n {\n level: 'info',\n }\n );\n }\n\n const chunkResult: JsonChunk[] = [];\n\n // Process chunks in parallel (globally throttled) to allow concurrent translation\n const chunkPromises = chunkedJsonContent.map(async (chunk) => {\n const chunkPreset = createChunkPreset(chunk.index, chunk.total);\n\n if (nbOfChunks > 1) {\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`,\n {\n level: 'info',\n }\n );\n }\n\n const reconstructedChunk = reconstructFromSingleChunk(chunk);\n const {\n extractedContent: chunkExtractedContent,\n translatableDictionary: chunkTranslatableDictionary,\n } = extractTranslatableContent(reconstructedChunk);\n\n const executeTranslation = async () => {\n return await retryManager(\n async () => {\n let translationResult: any;\n\n if (aiClient && aiConfig) {\n translationResult = await aiClient.translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiConfig,\n });\n } else {\n translationResult = await intlayerAPI.ai\n .translateJSON({\n entryFileContent:\n chunkTranslatableDictionary as unknown as JSON,\n presetOutputContent: chunkTranslatableDictionary,\n dictionaryDescription:\n dictionaryToProcess.description ??\n metadata?.description ??\n '',\n entryLocale: task.sourceLocale,\n outputLocale: targetLocale,\n mode,\n aiOptions,\n })\n .then((result) => result.data);\n }\n\n if (!translationResult?.fileContent) {\n throw new Error('No content result');\n }\n\n const { isIdentic, error } = verifyIdenticObjectFormat(\n translationResult.fileContent,\n chunkTranslatableDictionary\n );\n\n if (!isIdentic) {\n throw new Error(\n `Translation result does not match expected format: ${error}`\n );\n }\n\n notifySuccess();\n return reinsertTranslatedContent(\n reconstructedChunk,\n chunkExtractedContent,\n translationResult.fileContent as Record<number, string>\n );\n },\n {\n maxRetry: MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) => {\n const chunkPreset = createChunkPreset(\n chunk.index,\n chunk.total\n );\n appLogger(\n `${task.dictionaryPreset}${localePreset}${chunkPreset} ${colorize('Error filling:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n );\n\n followingErrors += 1;\n\n if (followingErrors >= MAX_FOLLOWING_ERRORS) {\n appLogger(`There is something wrong.`, {\n level: 'error',\n });\n process.exit(1); // 1 for error\n }\n },\n }\n )();\n };\n\n const wrapped = options?.onHandle\n ? options.onHandle(executeTranslation) // queued in global limiter\n : executeTranslation(); // no global limiter\n\n return wrapped.then((result) => ({ chunk, result }));\n });\n\n // Wait for all chunks for this locale in parallel (still capped by global limiter)\n const chunkResults = await Promise.all(chunkPromises);\n\n // Maintain order\n chunkResults\n .sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index)\n .forEach(({ result }) => {\n chunkResult.push(result);\n });\n\n // Merge translated chunk contents back into a single content object\n const reinsertedContent = mergeChunks(chunkResult);\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // Merge newly translated content (including explicit null fallbacks) back\n // into the existing target locale content. Applies to both per-locale and\n // multilingual paths so the target always retains previously translated\n // fields and receives null markers where the source has no translation.\n const finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n merged.content\n );\n\n return [targetLocale, finalContent] as const;\n })\n );\n\n const translatedContent: Partial<Record<Locale, Dictionary['content']>> =\n Object.fromEntries(translatedContentResults);\n\n const baseDictionary = baseUnmergedDictionary.locale\n ? {\n ...baseUnmergedDictionary,\n key: baseUnmergedDictionary.key!,\n content: {},\n }\n : baseUnmergedDictionary;\n\n let dictionaryOutput: Dictionary = {\n ...getMultilingualDictionary(baseDictionary),\n locale: undefined, // Ensure the dictionary is multilingual\n ...metadata,\n };\n\n for (const targetLocale of task.targetLocales) {\n if (translatedContent[targetLocale]) {\n dictionaryOutput = insertContentInDictionary(\n dictionaryOutput,\n translatedContent[targetLocale],\n targetLocale\n );\n }\n }\n\n appLogger(\n `${task.dictionaryPreset} ${colorize('Translation completed successfully', ANSIColors.GREEN)} for ${colorizePath(basename(dictionaryOutput.filePath!))}`,\n {\n level: 'info',\n }\n );\n\n // The dict-level `fill` value may have been lost during JSON serialisation\n // (functions can't be serialised to JSON). Fall back to the config-level\n // fill so that an explicit fill in intlayer.config.ts is honoured.\n const effectiveFillForCheck =\n baseUnmergedDictionary.fill ?? configuration.dictionary?.fill;\n\n if (\n baseUnmergedDictionary.locale &&\n (effectiveFillForCheck === true ||\n effectiveFillForCheck === undefined) &&\n baseUnmergedDictionary.location === 'local'\n ) {\n const dictionaryFilePath = baseUnmergedDictionary\n .filePath!.split('.')\n .slice(0, -1);\n\n const contentIndex = dictionaryFilePath[dictionaryFilePath.length - 1];\n\n return JSON.parse(\n JSON.stringify({\n ...task,\n dictionaryOutput: {\n ...dictionaryOutput,\n fill: undefined,\n filled: true,\n },\n }).replaceAll(\n new RegExp(`\\\\.${contentIndex}\\\\.[a-zA-Z0-9]+`, 'g'),\n `.filled.${contentIndex}.json`\n )\n );\n }\n\n return {\n ...task,\n dictionaryOutput,\n };\n },\n {\n maxRetry: GROUP_MAX_RETRY,\n delay: RETRY_DELAY,\n onError: ({ error, attempt, maxRetry }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Error:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)} - Attempt ${colorizeNumber(attempt + 1)} of ${colorizeNumber(maxRetry)}`,\n {\n level: 'error',\n }\n ),\n onMaxTryReached: ({ error }) =>\n appLogger(\n `${task.dictionaryPreset} ${colorize('Maximum number of retries reached:', ANSIColors.RED)} ${colorize(serializeError(error), ANSIColors.GREY_DARK)}`,\n {\n level: 'error',\n }\n ),\n }\n )();\n\n return result as TranslateDictionaryResult;\n};\n"],"mappings":";;;;;;;;;;;;AAyDA,MAAM,qBAAqB,YAAoB,gBAAwB;AACrE,KAAI,eAAe,EAAG,QAAO;AAC7B,QAAO,MACL;EACE,SAAS,KAAK,WAAW,UAAU;EACnC,eAAe,aAAa,EAAE;EAC9B,SAAS,IAAI,eAAe,WAAW,UAAU;EACjD,SAAS,KAAK,WAAW,UAAU;EACpC,CAAC,KAAK,GAAG,EACV,EAAE,SAAS,GAAG,CACf;;AAGH,MAAM,sBAAsB,eAC1B,CAAC,WAAW,eAAe,CAAC,WAAW,SAAS,CAAC,WAAW;AAE9D,MAAM,kBAAkB,UAA2B;AACjD,KAAI,iBAAiB,MACnB,QAAO,MAAM,QACT,GAAG,MAAM,QAAQ,WAAW,OAAO,MAAM,MAAM,CAAC,KAChD,MAAM;AAEZ,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI;AACF,SAAO,KAAK,UAAU,MAAM;SACtB;AACN,SAAO,OAAO,MAAM;;;AAIxB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,YAAY;AAClB,MAAM,cAAc,MAAO;AAE3B,MAAM,uBAAuB;AAC7B,IAAI,kBAAkB;AAEtB,MAAa,sBAAsB,OACjC,MACA,eACA,YACuC;CACvC,MAAM,YAAY,aAAa,cAAc;CAC7C,MAAM,cAAc,oBAAoB,QAAW,cAAc;CAEjE,MAAM,EAAE,MAAM,WAAW,cAAc,UAAU,aAAa;EAC5D,MAAM;EACN,cAAc;EACd,GAAG;EACJ;CAED,MAAM,sBAAsB;AAC1B,oBAAkB;AAClB,WAAS,aAAa;;AA2axB,QAAO,MAxac,aACnB,YAAY;EACV,MAAM,6BAA6B,wBAAwB,cAAc;EAEzE,MAAM,yBACJ,2BAA2B,KAAK,eAAe,MAC5C,SAAS,KAAK,YAAY,KAAK,kBACjC;AAEH,MAAI,CAAC,wBAAwB;AAC3B,aACE,GAAG,KAAK,iBAAiB,gEACzB,EACE,OAAO,QACR,CACF;AACD,UAAO;IAAE,GAAG;IAAM,kBAAkB;IAAM;;EAG5C,IAAI;AAIJ,MACE,iBACC,mBAAmB,uBAAuB,IAAI,SAAS,WACxD;GACA,MAAM,0BAA0B,uBAC9B,wBACA,cAAc,qBAAqB,cACpC;AAED,aACE,GAAG,KAAK,iBAAiB,gCAAgC,aAAa,SAAS,uBAAuB,SAAU,CAAC,IACjH,EACE,OAAO,QACR,CACF;GAED,MAAM,WAAW,YAAY;AAC3B,QAAI,YAAY,SAMd,QAAO,EACL,MAAM,MANa,SAAS,wBAAwB;KACpD,aAAa,KAAK,UAAU,wBAAwB;KACpD;KACD,CAAC,EAID;AAGH,WAAO,MAAM,YAAY,GAAG,gCAAgC;KAC1D,aAAa,KAAK,UAAU,wBAAwB;KACpD;KACD,CAAC;;AAOJ,eAJuB,SAAS,WAC5B,MAAM,QAAQ,SAAS,SAAS,GAChC,MAAM,UAAU,EAEM,MAAM;;EAGlC,MAAM,2BAA2B,MAAM,QAAQ,IAC7C,KAAK,cAAc,IAAI,OAAO,iBAAiB;;;;;;;;;;GAW7C,IAAI,sBAAsB,gBAAgB,uBAAuB;GAEjE,IAAI;AAEJ,OAAI,OAAO,uBAAuB,WAAW,UAAU;IAUrD,MAAM,2BAA2B,2BAC/B,KAAK,gBACJ,MACA,SACC,KAAK,WAAW,gBAChB,KAAK,aAAa,uBAAuB,SAC5C;IAID,MAAM,iBAAiB,uBAAuB,YAAY;IAC1D,MAAM,wBAAwB,eAAe,SAC3C,IAAI,KAAK,aAAa,GACvB,GACG,eAAe,QACb,IAAI,OAAO,IAAI,KAAK,aAAa,IAAI,IAAI,EACzC,IAAI,aAAa,GAClB,GACD,eAAe,QACb,IAAI,OAAO,QAAQ,KAAK,aAAa,aAAa,EAClD,KAAK,aAAa,IACnB;AAEL,6BAAyB,4BAA4B;KACnD,KAAK,uBAAuB;KAC5B,SAAS,EAAE;KACX,UAAU,yBAAyB;KACnC,QAAQ;KACT;UACI;AAEL,QAAI,SAAS,WAEX,uBAAsB,uCACpB,qBACA,aACD;AAGH,0BAAsB,uBACpB,qBACA,KAAK,aACN;AAED,6BAAyB,uBACvB,wBACA,aACD;;AAMH,OAAI,SAAS,WACX,uBAAsB;IACpB,GAAG;IACH,SACE,oBACE,oBAAoB,SACpB,uBAAuB,QACxB,IAAI,EAAE;IACV;GAGH,MAAM,eAAe,MACnB;IACE,SAAS,KAAK,WAAW,UAAU;IACnC,aAAa,aAAa;IAC1B,SAAS,KAAK,WAAW,UAAU;IACpC,CAAC,KAAK,GAAG,EACV,EAAE,SAAS,IAAI,CAChB;AAED,aACE,GAAG,KAAK,mBAAmB,aAAa,aAAa,aAAa,SAAS,uBAAuB,SAAU,CAAC,IAC7G,EACE,OAAO,QACR,CACF;GAED,MAAM,qBAAkC,UACtC,oBAAoB,SACpB,WACD;GAED,MAAM,aAAa,mBAAmB;AAEtC,OAAI,aAAa,EACf,WACE,GAAG,KAAK,mBAAmB,aAAa,cAAc,eAAe,WAAW,CAAC,0BACjF,EACE,OAAO,QACR,CACF;GAGH,MAAM,cAA2B,EAAE;GAGnC,MAAM,gBAAgB,mBAAmB,IAAI,OAAO,UAAU;IAC5D,MAAM,cAAc,kBAAkB,MAAM,OAAO,MAAM,MAAM;AAE/D,QAAI,aAAa,EACf,WACE,GAAG,KAAK,mBAAmB,eAAe,YAAY,qBACtD,EACE,OAAO,QACR,CACF;IAGH,MAAM,qBAAqB,2BAA2B,MAAM;IAC5D,MAAM,EACJ,kBAAkB,uBAClB,wBAAwB,gCACtB,2BAA2B,mBAAmB;IAElD,MAAM,qBAAqB,YAAY;AACrC,YAAO,MAAM,aACX,YAAY;MACV,IAAI;AAEJ,UAAI,YAAY,SACd,qBAAoB,MAAM,SAAS,cAAc;OAC/C,kBACE;OACF,qBAAqB;OACrB,uBACE,oBAAoB,eACpB,UAAU,eACV;OACF,aAAa,KAAK;OAClB,cAAc;OACd;OACA;OACD,CAAC;UAEF,qBAAoB,MAAM,YAAY,GACnC,cAAc;OACb,kBACE;OACF,qBAAqB;OACrB,uBACE,oBAAoB,eACpB,UAAU,eACV;OACF,aAAa,KAAK;OAClB,cAAc;OACd;OACA;OACD,CAAC,CACD,MAAM,WAAW,OAAO,KAAK;AAGlC,UAAI,CAAC,mBAAmB,YACtB,OAAM,IAAI,MAAM,oBAAoB;MAGtC,MAAM,EAAE,WAAW,UAAU,0BAC3B,kBAAkB,aAClB,4BACD;AAED,UAAI,CAAC,UACH,OAAM,IAAI,MACR,sDAAsD,QACvD;AAGH,qBAAe;AACf,aAAO,0BACL,oBACA,uBACA,kBAAkB,YACnB;QAEH;MACE,UAAU;MACV,OAAO;MACP,UAAU,EAAE,OAAO,SAAS,eAAe;OACzC,MAAM,cAAc,kBAClB,MAAM,OACN,MAAM,MACP;AACD,iBACE,GAAG,KAAK,mBAAmB,eAAe,YAAY,GAAG,SAAS,kBAAkB,WAAW,IAAI,CAAC,GAAG,SAAS,eAAe,MAAM,EAAE,WAAW,UAAU,CAAC,aAAa,eAAe,UAAU,EAAE,CAAC,MAAM,eAAe,SAAS,IACpO,EACE,OAAO,SACR,CACF;AAED,0BAAmB;AAEnB,WAAI,mBAAmB,sBAAsB;AAC3C,kBAAU,6BAA6B,EACrC,OAAO,SACR,CAAC;AACF,gBAAQ,KAAK,EAAE;;;MAGpB,CACF,EAAE;;AAOL,YAJgB,SAAS,WACrB,QAAQ,SAAS,mBAAmB,GACpC,oBAAoB,EAET,MAAM,YAAY;KAAE;KAAO;KAAQ,EAAE;KACpD;AAMF,UAH2B,QAAQ,IAAI,cAAc,EAIlD,MAAM,QAAQ,WAAW,OAAO,MAAM,QAAQ,OAAO,MAAM,MAAM,CACjE,SAAS,EAAE,aAAa;AACvB,gBAAY,KAAK,OAAO;KACxB;GAGJ,MAAM,oBAAoB,YAAY,YAAY;GAElD,MAAM,SAAS;IACb,GAAG;IACH,SAAS;IACV;AAWD,UAAO,CAAC,cALa,iBACnB,uBAAuB,WAAW,EAAE,EACpC,OAAO,QAGyB,CAAC;IACnC,CACH;EAED,MAAM,oBACJ,OAAO,YAAY,yBAAyB;EAU9C,IAAI,mBAA+B;GACjC,GAAG,0BATkB,uBAAuB,SAC1C;IACE,GAAG;IACH,KAAK,uBAAuB;IAC5B,SAAS,EAAE;IACZ,GACD,uBAG0C;GAC5C,QAAQ;GACR,GAAG;GACJ;AAED,OAAK,MAAM,gBAAgB,KAAK,cAC9B,KAAI,kBAAkB,cACpB,oBAAmB,0BACjB,kBACA,kBAAkB,eAClB,aACD;AAIL,YACE,GAAG,KAAK,iBAAiB,GAAG,SAAS,sCAAsC,WAAW,MAAM,CAAC,OAAO,aAAa,SAAS,iBAAiB,SAAU,CAAC,IACtJ,EACE,OAAO,QACR,CACF;EAKD,MAAM,wBACJ,uBAAuB,QAAQ,cAAc,YAAY;AAE3D,MACE,uBAAuB,WACtB,0BAA0B,QACzB,0BAA0B,WAC5B,uBAAuB,aAAa,SACpC;GACA,MAAM,qBAAqB,uBACxB,SAAU,MAAM,IAAI,CACpB,MAAM,GAAG,GAAG;GAEf,MAAM,eAAe,mBAAmB,mBAAmB,SAAS;AAEpE,UAAO,KAAK,MACV,KAAK,UAAU;IACb,GAAG;IACH,kBAAkB;KAChB,GAAG;KACH,MAAM;KACN,QAAQ;KACT;IACF,CAAC,CAAC,WACD,IAAI,OAAO,MAAM,aAAa,kBAAkB,IAAI,EACpD,WAAW,aAAa,OACzB,CACF;;AAGH,SAAO;GACL,GAAG;GACH;GACD;IAEH;EACE,UAAU;EACV,OAAO;EACP,UAAU,EAAE,OAAO,SAAS,eAC1B,UACE,GAAG,KAAK,iBAAiB,GAAG,SAAS,UAAU,WAAW,IAAI,CAAC,GAAG,SAAS,eAAe,MAAM,EAAE,WAAW,UAAU,CAAC,aAAa,eAAe,UAAU,EAAE,CAAC,MAAM,eAAe,SAAS,IAC/L,EACE,OAAO,SACR,CACF;EACH,kBAAkB,EAAE,YAClB,UACE,GAAG,KAAK,iBAAiB,GAAG,SAAS,sCAAsC,WAAW,IAAI,CAAC,GAAG,SAAS,eAAe,MAAM,EAAE,WAAW,UAAU,IACnJ,EACE,OAAO,SACR,CACF;EACJ,CACF,EAAE"}
@@ -1,6 +1,6 @@
1
1
  import { formatFillData } from "./formatFillData.mjs";
2
2
  import { getAvailableLocalesInDictionary } from "./getAvailableLocalesInDictionary.mjs";
3
- import { join } from "node:path";
3
+ import { resolve } from "node:path";
4
4
  import { writeContentDeclaration } from "@intlayer/chokidar/build";
5
5
  import { formatLocale, formatPath } from "@intlayer/chokidar/utils";
6
6
  import { colorizeKey, getAppLogger } from "@intlayer/config/logger";
@@ -39,7 +39,7 @@ const writeFill = async (contentDeclarationFile, outputLocales, parentLocales, c
39
39
  filled: true,
40
40
  locale: output.isPerLocale ? output.localeList[0] : void 0,
41
41
  localId: `${contentDeclarationFile.key}::local::${output.filePath}`,
42
- filePath: join(configuration.system.baseDir, output.filePath)
42
+ filePath: resolve(configuration.system.baseDir, output.filePath)
43
43
  }, configuration, { localeList: output.isPerLocale ? output.localeList : outputLocales ?? configuration.internationalization.locales });
44
44
  if (output.isPerLocale) appLogger(`Auto filled per-locale content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)} for locale ${formatLocale(output.localeList[0])}`, { level: "info" });
45
45
  else appLogger(`Auto filled content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)}`, { level: "info" });
@@ -1 +1 @@
1
- {"version":3,"file":"writeFill.mjs","names":[],"sources":["../../../src/fill/writeFill.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { writeContentDeclaration } from '@intlayer/chokidar/build';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary, Fill } from '@intlayer/types/dictionary';\nimport { type FillData, formatFillData } from './formatFillData';\nimport { getAvailableLocalesInDictionary } from './getAvailableLocalesInDictionary';\n\nexport const writeFill = async (\n contentDeclarationFile: Dictionary,\n outputLocales: Locale[],\n parentLocales: Locale[],\n configuration: IntlayerConfig\n) => {\n const appLogger = getAppLogger(configuration);\n const dictionaries = getDictionaries(configuration);\n\n const fullDictionary = dictionaries[contentDeclarationFile.key];\n\n const { filePath } = contentDeclarationFile;\n\n if (!filePath) {\n appLogger('No file path found for dictionary', {\n level: 'error',\n });\n return;\n }\n\n const fillOptions: Fill | undefined =\n contentDeclarationFile.fill ?? configuration.dictionary?.fill ?? true;\n\n if ((fillOptions as boolean) === false) {\n appLogger(\n `Auto fill is disabled for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const requestedLocales: Locale[] = (\n outputLocales ?? configuration.internationalization.locales\n ).filter((locale) => !parentLocales?.includes(locale));\n\n // Get locales that actually have translations in the content\n const availableLocales = getAvailableLocalesInDictionary(\n contentDeclarationFile\n );\n\n // Only write files for locales that have actual translations\n const localeList = requestedLocales.filter((locale) =>\n availableLocales.includes(locale)\n );\n\n if (localeList.length === 0) {\n appLogger(\n `No translations available for dictionary '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const fillData: FillData[] = await formatFillData(\n fillOptions as Fill,\n localeList,\n filePath,\n fullDictionary.key,\n configuration\n );\n\n for await (const output of fillData) {\n if (!output.filePath) {\n appLogger(\n `No file path found for auto filled content declaration for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'error',\n }\n );\n continue;\n }\n\n const { fill, ...rest } = contentDeclarationFile;\n\n await writeContentDeclaration(\n {\n ...rest,\n filled: true,\n locale: output.isPerLocale ? output.localeList[0] : undefined,\n localId: `${contentDeclarationFile.key}::local::${output.filePath}`,\n filePath: join(configuration.system.baseDir, output.filePath), // Use absolute path for vscode extension\n },\n configuration,\n {\n // Per-locale files: write only the specific locale.\n // Multilingual files (string/function fill, single output): include all\n // output locales (including source) so the file is complete.\n localeList: output.isPerLocale\n ? output.localeList\n : (outputLocales ?? configuration.internationalization.locales),\n }\n );\n\n if (output.isPerLocale) {\n appLogger(\n `Auto filled per-locale content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)} for locale ${formatLocale(output.localeList[0])}`,\n { level: 'info' }\n );\n } else {\n appLogger(\n `Auto filled content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)}`,\n { level: 'info' }\n );\n }\n }\n};\n"],"mappings":";;;;;;;;;AAWA,MAAa,YAAY,OACvB,wBACA,eACA,eACA,kBACG;CACH,MAAM,YAAY,aAAa,cAAc;CAG7C,MAAM,iBAFe,gBAAgB,cAEF,CAAC,uBAAuB;CAE3D,MAAM,EAAE,aAAa;AAErB,KAAI,CAAC,UAAU;AACb,YAAU,qCAAqC,EAC7C,OAAO,SACR,CAAC;AACF;;CAGF,MAAM,cACJ,uBAAuB,QAAQ,cAAc,YAAY,QAAQ;AAEnE,KAAK,gBAA4B,OAAO;AACtC,YACE,8BAA8B,YAAY,eAAe,IAAI,CAAC,IAC9D,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,oBACJ,iBAAiB,cAAc,qBAAqB,SACpD,QAAQ,WAAW,CAAC,eAAe,SAAS,OAAO,CAAC;CAGtD,MAAM,mBAAmB,gCACvB,uBACD;CAGD,MAAM,aAAa,iBAAiB,QAAQ,WAC1C,iBAAiB,SAAS,OAAO,CAClC;AAED,KAAI,WAAW,WAAW,GAAG;AAC3B,YACE,6CAA6C,YAAY,eAAe,IAAI,CAAC,IAC7E,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,WAAuB,MAAM,eACjC,aACA,YACA,UACA,eAAe,KACf,cACD;AAED,YAAW,MAAM,UAAU,UAAU;AACnC,MAAI,CAAC,OAAO,UAAU;AACpB,aACE,+DAA+D,YAAY,eAAe,IAAI,CAAC,IAC/F,EACE,OAAO,SACR,CACF;AACD;;EAGF,MAAM,EAAE,MAAM,GAAG,SAAS;AAE1B,QAAM,wBACJ;GACE,GAAG;GACH,QAAQ;GACR,QAAQ,OAAO,cAAc,OAAO,WAAW,KAAK;GACpD,SAAS,GAAG,uBAAuB,IAAI,WAAW,OAAO;GACzD,UAAU,KAAK,cAAc,OAAO,SAAS,OAAO,SAAS;GAC9D,EACD,eACA,EAIE,YAAY,OAAO,cACf,OAAO,aACN,iBAAiB,cAAc,qBAAqB,SAC1D,CACF;AAED,MAAI,OAAO,YACT,WACE,mDAAmD,YAAY,eAAe,IAAI,CAAC,eAAe,WAAW,OAAO,SAAS,CAAC,cAAc,aAAa,OAAO,WAAW,GAAG,IAC9K,EAAE,OAAO,QAAQ,CAClB;MAED,WACE,wCAAwC,YAAY,eAAe,IAAI,CAAC,eAAe,WAAW,OAAO,SAAS,IAClH,EAAE,OAAO,QAAQ,CAClB"}
1
+ {"version":3,"file":"writeFill.mjs","names":[],"sources":["../../../src/fill/writeFill.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { writeContentDeclaration } from '@intlayer/chokidar/build';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary, Fill } from '@intlayer/types/dictionary';\nimport { type FillData, formatFillData } from './formatFillData';\nimport { getAvailableLocalesInDictionary } from './getAvailableLocalesInDictionary';\n\nexport const writeFill = async (\n contentDeclarationFile: Dictionary,\n outputLocales: Locale[],\n parentLocales: Locale[],\n configuration: IntlayerConfig\n) => {\n const appLogger = getAppLogger(configuration);\n const dictionaries = getDictionaries(configuration);\n\n const fullDictionary = dictionaries[contentDeclarationFile.key];\n\n const { filePath } = contentDeclarationFile;\n\n if (!filePath) {\n appLogger('No file path found for dictionary', {\n level: 'error',\n });\n return;\n }\n\n const fillOptions: Fill | undefined =\n contentDeclarationFile.fill ?? configuration.dictionary?.fill ?? true;\n\n if ((fillOptions as boolean) === false) {\n appLogger(\n `Auto fill is disabled for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const requestedLocales: Locale[] = (\n outputLocales ?? configuration.internationalization.locales\n ).filter((locale) => !parentLocales?.includes(locale));\n\n // Get locales that actually have translations in the content\n const availableLocales = getAvailableLocalesInDictionary(\n contentDeclarationFile\n );\n\n // Only write files for locales that have actual translations\n const localeList = requestedLocales.filter((locale) =>\n availableLocales.includes(locale)\n );\n\n if (localeList.length === 0) {\n appLogger(\n `No translations available for dictionary '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'info',\n }\n );\n return;\n }\n\n const fillData: FillData[] = await formatFillData(\n fillOptions as Fill,\n localeList,\n filePath,\n fullDictionary.key,\n configuration\n );\n\n for await (const output of fillData) {\n if (!output.filePath) {\n appLogger(\n `No file path found for auto filled content declaration for '${colorizeKey(fullDictionary.key)}'`,\n {\n level: 'error',\n }\n );\n continue;\n }\n\n const { fill, ...rest } = contentDeclarationFile;\n\n await writeContentDeclaration(\n {\n ...rest,\n filled: true,\n locale: output.isPerLocale ? output.localeList[0] : undefined,\n localId: `${contentDeclarationFile.key}::local::${output.filePath}`,\n filePath: resolve(configuration.system.baseDir, output.filePath), // Use absolute path for vscode extension\n },\n configuration,\n {\n // Per-locale files: write only the specific locale.\n // Multilingual files (string/function fill, single output): include all\n // output locales (including source) so the file is complete.\n localeList: output.isPerLocale\n ? output.localeList\n : (outputLocales ?? configuration.internationalization.locales),\n }\n );\n\n if (output.isPerLocale) {\n appLogger(\n `Auto filled per-locale content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)} for locale ${formatLocale(output.localeList[0])}`,\n { level: 'info' }\n );\n } else {\n appLogger(\n `Auto filled content declaration for '${colorizeKey(fullDictionary.key)}' written to ${formatPath(output.filePath)}`,\n { level: 'info' }\n );\n }\n }\n};\n"],"mappings":";;;;;;;;;AAWA,MAAa,YAAY,OACvB,wBACA,eACA,eACA,kBACG;CACH,MAAM,YAAY,aAAa,cAAc;CAG7C,MAAM,iBAFe,gBAAgB,cAEF,CAAC,uBAAuB;CAE3D,MAAM,EAAE,aAAa;AAErB,KAAI,CAAC,UAAU;AACb,YAAU,qCAAqC,EAC7C,OAAO,SACR,CAAC;AACF;;CAGF,MAAM,cACJ,uBAAuB,QAAQ,cAAc,YAAY,QAAQ;AAEnE,KAAK,gBAA4B,OAAO;AACtC,YACE,8BAA8B,YAAY,eAAe,IAAI,CAAC,IAC9D,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,oBACJ,iBAAiB,cAAc,qBAAqB,SACpD,QAAQ,WAAW,CAAC,eAAe,SAAS,OAAO,CAAC;CAGtD,MAAM,mBAAmB,gCACvB,uBACD;CAGD,MAAM,aAAa,iBAAiB,QAAQ,WAC1C,iBAAiB,SAAS,OAAO,CAClC;AAED,KAAI,WAAW,WAAW,GAAG;AAC3B,YACE,6CAA6C,YAAY,eAAe,IAAI,CAAC,IAC7E,EACE,OAAO,QACR,CACF;AACD;;CAGF,MAAM,WAAuB,MAAM,eACjC,aACA,YACA,UACA,eAAe,KACf,cACD;AAED,YAAW,MAAM,UAAU,UAAU;AACnC,MAAI,CAAC,OAAO,UAAU;AACpB,aACE,+DAA+D,YAAY,eAAe,IAAI,CAAC,IAC/F,EACE,OAAO,SACR,CACF;AACD;;EAGF,MAAM,EAAE,MAAM,GAAG,SAAS;AAE1B,QAAM,wBACJ;GACE,GAAG;GACH,QAAQ;GACR,QAAQ,OAAO,cAAc,OAAO,WAAW,KAAK;GACpD,SAAS,GAAG,uBAAuB,IAAI,WAAW,OAAO;GACzD,UAAU,QAAQ,cAAc,OAAO,SAAS,OAAO,SAAS;GACjE,EACD,eACA,EAIE,YAAY,OAAO,cACf,OAAO,aACN,iBAAiB,cAAc,qBAAqB,SAC1D,CACF;AAED,MAAI,OAAO,YACT,WACE,mDAAmD,YAAY,eAAe,IAAI,CAAC,eAAe,WAAW,OAAO,SAAS,CAAC,cAAc,aAAa,OAAO,WAAW,GAAG,IAC9K,EAAE,OAAO,QAAQ,CAClB;MAED,WACE,wCAAwC,YAAY,eAAe,IAAI,CAAC,eAAe,WAAW,OAAO,SAAS,IAClH,EAAE,OAAO,QAAQ,CAClB"}
@@ -1,8 +1,9 @@
1
+ import { IntlayerConfig } from "@intlayer/types/config";
1
2
  import { FilePathPattern } from "@intlayer/types/filePathPattern";
2
3
  import { Locale } from "@intlayer/types/allLocales";
3
4
 
4
5
  //#region src/fill/formatAutoFilledFilePath.d.ts
5
- declare const formatAutoFilledFilePath: (autoFillField: FilePathPattern, dictionaryKey: string, dictionaryFilePath: string, baseDir: string, locale?: Locale) => Promise<string>;
6
+ declare const formatAutoFilledFilePath: (autoFillField: FilePathPattern, dictionaryKey: string, dictionaryFilePath: string, baseDir: string, configuration: IntlayerConfig, locale?: Locale) => Promise<string>;
6
7
  //#endregion
7
8
  export { formatAutoFilledFilePath };
8
9
  //# sourceMappingURL=formatAutoFilledFilePath.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatAutoFilledFilePath.d.ts","names":[],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"mappings":";;;;cAKa,wBAAA,GACX,aAAA,EAAe,eAAA,EACf,aAAA,UACA,kBAAA,UACA,OAAA,UACA,MAAA,GAAS,MAAA,KACR,OAAA"}
1
+ {"version":3,"file":"formatAutoFilledFilePath.d.ts","names":[],"sources":["../../../src/fill/formatAutoFilledFilePath.ts"],"mappings":";;;;;cASa,wBAAA,GACX,aAAA,EAAe,eAAA,EACf,aAAA,UACA,kBAAA,UACA,OAAA,UACA,aAAA,EAAe,cAAA,EACf,MAAA,GAAS,MAAA,KACR,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/cli",
3
- "version": "8.9.0",
3
+ "version": "8.9.2",
4
4
  "private": false,
5
5
  "description": "Provides uniform command-line interface scripts for Intlayer, used in packages like intlayer-cli and intlayer.",
6
6
  "keywords": [
@@ -67,22 +67,22 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@clack/prompts": "0.11.0",
70
- "@intlayer/api": "8.9.0",
71
- "@intlayer/babel": "8.9.0",
72
- "@intlayer/chokidar": "8.9.0",
73
- "@intlayer/config": "8.9.0",
74
- "@intlayer/core": "8.9.0",
75
- "@intlayer/dictionaries-entry": "8.9.0",
76
- "@intlayer/remote-dictionaries-entry": "8.9.0",
77
- "@intlayer/types": "8.9.0",
78
- "@intlayer/unmerged-dictionaries-entry": "8.9.0",
70
+ "@intlayer/api": "8.9.2",
71
+ "@intlayer/babel": "8.9.2",
72
+ "@intlayer/chokidar": "8.9.2",
73
+ "@intlayer/config": "8.9.2",
74
+ "@intlayer/core": "8.9.2",
75
+ "@intlayer/dictionaries-entry": "8.9.2",
76
+ "@intlayer/remote-dictionaries-entry": "8.9.2",
77
+ "@intlayer/types": "8.9.2",
78
+ "@intlayer/unmerged-dictionaries-entry": "8.9.2",
79
79
  "commander": "14.0.3",
80
80
  "enquirer": "^2.4.1",
81
81
  "eventsource": "4.1.0",
82
82
  "fast-glob": "3.3.3"
83
83
  },
84
84
  "devDependencies": {
85
- "@intlayer/ai": "8.9.0",
85
+ "@intlayer/ai": "8.9.2",
86
86
  "@types/node": "25.6.0",
87
87
  "@utils/ts-config": "1.0.4",
88
88
  "@utils/ts-config-types": "1.0.4",
@@ -93,7 +93,7 @@
93
93
  "vitest": "4.1.5"
94
94
  },
95
95
  "peerDependencies": {
96
- "@intlayer/ai": "8.9.0"
96
+ "@intlayer/ai": "8.9.2"
97
97
  },
98
98
  "peerDependenciesMeta": {
99
99
  "@intlayer/ai": {