@intlayer/cli 8.6.2 → 8.6.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"IntlayerEventListener.cjs","names":["configuration","EventSource"],"sources":["../../src/IntlayerEventListener.ts"],"sourcesContent":["import { getIntlayerAPIProxy } from '@intlayer/api';\n// @ts-ignore: @intlayer/backend is not built yet\nimport type { DictionaryAPI, MessageEventData } from '@intlayer/backend';\nimport configuration from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { EventSource } from 'eventsource';\n\nexport type IntlayerMessageEvent = MessageEvent;\n\n/**\n * IntlayerEventListener class to listen for dictionary changes via SSE (Server-Sent Events).\n *\n * Usage example:\n *\n * import { buildIntlayerDictionary } from './transpiler/declaration_file_to_dictionary/intlayer_dictionary';\n * import { IntlayerEventListener } from '@intlayer/api';\n *\n * export const checkDictionaryChanges = async () => {\n * // Instantiate the listener\n * const eventListener = new IntlayerEventListener();\n *\n * // Set up your callbacks\n * eventListener.onDictionaryChange = async (dictionary) => {\n * await buildIntlayerDictionary(dictionary);\n * };\n *\n * // Initialize the listener\n * await eventListener.initialize();\n *\n * // Optionally, clean up later when you’re done\n * // eventListener.cleanup();\n * };\n */\nexport class IntlayerEventListener {\n private appLogger = getAppLogger(configuration);\n\n private eventSource: EventSource | null = null;\n private reconnectAttempts = 0;\n private maxReconnectAttempts = 5;\n private reconnectDelay = 1000; // Start with 1 second\n private isManuallyDisconnected = false;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Callback triggered when a Dictionary is ADDED.\n */\n public onDictionaryAdded?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is UPDATED.\n */\n public onDictionaryChange?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is DELETED.\n */\n public onDictionaryDeleted?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when connection is established or re-established.\n */\n public onConnectionOpen?: () => any;\n\n /**\n * Callback triggered when connection encounters an error.\n */\n public onConnectionError?: (error: Event) => any;\n\n constructor(private intlayerConfig: IntlayerConfig = configuration) {\n this.appLogger = getAppLogger(this.intlayerConfig);\n }\n\n /**\n * Initializes the EventSource connection using the given intlayerConfig\n * (or the default config if none was provided).\n */\n public async initialize(): Promise<void> {\n this.isManuallyDisconnected = false;\n await this.connect();\n }\n\n /**\n * Establishes the EventSource connection with automatic reconnection support.\n */\n private async connect(): Promise<void> {\n try {\n const backendURL = this.intlayerConfig.editor.backendURL;\n\n // Retrieve the access token via proxy\n const accessToken = await getIntlayerAPIProxy(\n undefined,\n this.intlayerConfig\n ).oAuth.getOAuth2AccessToken();\n\n if (!accessToken) {\n throw new Error('Failed to retrieve access token');\n }\n\n const API_ROUTE = `${backendURL}/api/event-listener`;\n\n // Close existing connection if any\n if (this.eventSource) {\n this.eventSource.close();\n }\n\n this.eventSource = new EventSource(API_ROUTE, {\n fetch: (input, init) =>\n fetch(input, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n Authorization: `Bearer ${accessToken.data?.accessToken}`,\n },\n }),\n });\n\n this.eventSource.onopen = () => {\n this.reconnectAttempts = 0;\n this.reconnectDelay = 1000; // Reset delay\n this.onConnectionOpen?.();\n };\n\n this.eventSource.onmessage = (event) => this.handleMessage(event);\n this.eventSource.onerror = (event) => this.handleError(event);\n } catch (_error) {\n this.appLogger('Failed to establish EventSource connection:', {\n level: 'error',\n });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Cleans up (closes) the EventSource connection.\n */\n public cleanup(): void {\n this.isManuallyDisconnected = true;\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n }\n\n /**\n * Schedules a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(): void {\n if (\n this.isManuallyDisconnected ||\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n this.appLogger(\n [\n `Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`,\n ],\n {\n level: 'error',\n }\n );\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = this.reconnectDelay * 2 ** (this.reconnectAttempts - 1); // Exponential backoff\n\n this.appLogger(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`\n );\n\n this.reconnectTimeout = setTimeout(async () => {\n if (!this.isManuallyDisconnected) {\n await this.connect();\n }\n }, delay);\n }\n\n /**\n * Handles incoming SSE messages, parses the event data,\n * and invokes the appropriate callback.\n */\n private async handleMessage(event: IntlayerMessageEvent): Promise<void> {\n try {\n const { data } = event;\n\n const dataJSON: MessageEventData[] = JSON.parse(data);\n\n for (const dataEl of dataJSON) {\n switch (dataEl.object) {\n case 'DICTIONARY':\n switch (dataEl.status) {\n case 'ADDED':\n await this.onDictionaryAdded?.(dataEl.data);\n break;\n case 'UPDATED':\n await this.onDictionaryChange?.(dataEl.data);\n break;\n case 'DELETED':\n await this.onDictionaryDeleted?.(dataEl.data);\n break;\n default:\n this.appLogger(\n ['Unhandled dictionary status:', dataEl.status],\n {\n level: 'error',\n }\n );\n break;\n }\n break;\n default:\n this.appLogger(['Unknown object type:', dataEl.object], {\n level: 'error',\n });\n break;\n }\n }\n } catch (error) {\n this.appLogger(['Error processing dictionary update:', error], {\n level: 'error',\n });\n }\n }\n\n /**\n * Handles any SSE errors and attempts reconnection if appropriate.\n */\n private handleError(event: Event): void {\n const errorEvent = event as any;\n\n // Log detailed error information\n this.appLogger(\n [\n 'EventSource error:',\n {\n type: errorEvent.type,\n message: errorEvent.message,\n code: errorEvent.code,\n readyState: this.eventSource?.readyState,\n url: this.eventSource?.url,\n },\n ],\n {\n level: 'error',\n }\n );\n\n // Notify error callback\n this.onConnectionError?.(event);\n\n // Check if this is a connection close error\n const isConnectionClosed =\n errorEvent.type === 'error' &&\n (errorEvent.message?.includes('terminated') ||\n errorEvent.message?.includes('closed') ||\n this.eventSource?.readyState === EventSource.CLOSED);\n\n if (isConnectionClosed && !this.isManuallyDisconnected) {\n this.appLogger(\n 'Connection was terminated by server, attempting to reconnect...'\n );\n this.scheduleReconnect();\n } else {\n // For other types of errors, close the connection\n this.cleanup();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"IntlayerEventListener.cjs","names":["configuration","EventSource"],"sources":["../../src/IntlayerEventListener.ts"],"sourcesContent":["import { getIntlayerAPIProxy } from '@intlayer/api';\n// @ts-ignore: @intlayer/backend is not built yet\nimport type { DictionaryAPI, MessageEventData } from '@intlayer/backend';\nimport { default as configuration } from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { EventSource } from 'eventsource';\n\nexport type IntlayerMessageEvent = MessageEvent;\n\n/**\n * IntlayerEventListener class to listen for dictionary changes via SSE (Server-Sent Events).\n *\n * Usage example:\n *\n * import { buildIntlayerDictionary } from './transpiler/declaration_file_to_dictionary/intlayer_dictionary';\n * import { IntlayerEventListener } from '@intlayer/api';\n *\n * export const checkDictionaryChanges = async () => {\n * // Instantiate the listener\n * const eventListener = new IntlayerEventListener();\n *\n * // Set up your callbacks\n * eventListener.onDictionaryChange = async (dictionary) => {\n * await buildIntlayerDictionary(dictionary);\n * };\n *\n * // Initialize the listener\n * await eventListener.initialize();\n *\n * // Optionally, clean up later when you’re done\n * // eventListener.cleanup();\n * };\n */\nexport class IntlayerEventListener {\n private appLogger = getAppLogger(configuration);\n\n private eventSource: EventSource | null = null;\n private reconnectAttempts = 0;\n private maxReconnectAttempts = 5;\n private reconnectDelay = 1000; // Start with 1 second\n private isManuallyDisconnected = false;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Callback triggered when a Dictionary is ADDED.\n */\n public onDictionaryAdded?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is UPDATED.\n */\n public onDictionaryChange?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is DELETED.\n */\n public onDictionaryDeleted?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when connection is established or re-established.\n */\n public onConnectionOpen?: () => any;\n\n /**\n * Callback triggered when connection encounters an error.\n */\n public onConnectionError?: (error: Event) => any;\n\n constructor(private intlayerConfig: IntlayerConfig = configuration) {\n this.appLogger = getAppLogger(this.intlayerConfig);\n }\n\n /**\n * Initializes the EventSource connection using the given intlayerConfig\n * (or the default config if none was provided).\n */\n public async initialize(): Promise<void> {\n this.isManuallyDisconnected = false;\n await this.connect();\n }\n\n /**\n * Establishes the EventSource connection with automatic reconnection support.\n */\n private async connect(): Promise<void> {\n try {\n const backendURL = this.intlayerConfig.editor.backendURL;\n\n // Retrieve the access token via proxy\n const accessToken = await getIntlayerAPIProxy(\n undefined,\n this.intlayerConfig\n ).oAuth.getOAuth2AccessToken();\n\n if (!accessToken) {\n throw new Error('Failed to retrieve access token');\n }\n\n const API_ROUTE = `${backendURL}/api/event-listener`;\n\n // Close existing connection if any\n if (this.eventSource) {\n this.eventSource.close();\n }\n\n this.eventSource = new EventSource(API_ROUTE, {\n fetch: (input, init) =>\n fetch(input, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n Authorization: `Bearer ${accessToken.data?.accessToken}`,\n },\n }),\n });\n\n this.eventSource.onopen = () => {\n this.reconnectAttempts = 0;\n this.reconnectDelay = 1000; // Reset delay\n this.onConnectionOpen?.();\n };\n\n this.eventSource.onmessage = (event) => this.handleMessage(event);\n this.eventSource.onerror = (event) => this.handleError(event);\n } catch (_error) {\n this.appLogger('Failed to establish EventSource connection:', {\n level: 'error',\n });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Cleans up (closes) the EventSource connection.\n */\n public cleanup(): void {\n this.isManuallyDisconnected = true;\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n }\n\n /**\n * Schedules a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(): void {\n if (\n this.isManuallyDisconnected ||\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n this.appLogger(\n [\n `Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`,\n ],\n {\n level: 'error',\n }\n );\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = this.reconnectDelay * 2 ** (this.reconnectAttempts - 1); // Exponential backoff\n\n this.appLogger(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`\n );\n\n this.reconnectTimeout = setTimeout(async () => {\n if (!this.isManuallyDisconnected) {\n await this.connect();\n }\n }, delay);\n }\n\n /**\n * Handles incoming SSE messages, parses the event data,\n * and invokes the appropriate callback.\n */\n private async handleMessage(event: IntlayerMessageEvent): Promise<void> {\n try {\n const { data } = event;\n\n const dataJSON: MessageEventData[] = JSON.parse(data);\n\n for (const dataEl of dataJSON) {\n switch (dataEl.object) {\n case 'DICTIONARY':\n switch (dataEl.status) {\n case 'ADDED':\n await this.onDictionaryAdded?.(dataEl.data);\n break;\n case 'UPDATED':\n await this.onDictionaryChange?.(dataEl.data);\n break;\n case 'DELETED':\n await this.onDictionaryDeleted?.(dataEl.data);\n break;\n default:\n this.appLogger(\n ['Unhandled dictionary status:', dataEl.status],\n {\n level: 'error',\n }\n );\n break;\n }\n break;\n default:\n this.appLogger(['Unknown object type:', dataEl.object], {\n level: 'error',\n });\n break;\n }\n }\n } catch (error) {\n this.appLogger(['Error processing dictionary update:', error], {\n level: 'error',\n });\n }\n }\n\n /**\n * Handles any SSE errors and attempts reconnection if appropriate.\n */\n private handleError(event: Event): void {\n const errorEvent = event as any;\n\n // Log detailed error information\n this.appLogger(\n [\n 'EventSource error:',\n {\n type: errorEvent.type,\n message: errorEvent.message,\n code: errorEvent.code,\n readyState: this.eventSource?.readyState,\n url: this.eventSource?.url,\n },\n ],\n {\n level: 'error',\n }\n );\n\n // Notify error callback\n this.onConnectionError?.(event);\n\n // Check if this is a connection close error\n const isConnectionClosed =\n errorEvent.type === 'error' &&\n (errorEvent.message?.includes('terminated') ||\n errorEvent.message?.includes('closed') ||\n this.eventSource?.readyState === EventSource.CLOSED);\n\n if (isConnectionClosed && !this.isManuallyDisconnected) {\n this.appLogger(\n 'Connection was terminated by server, attempting to reconnect...'\n );\n this.scheduleReconnect();\n } else {\n // For other types of errors, close the connection\n this.cleanup();\n }\n }\n}\n"],"mappings":"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"}
@@ -16,8 +16,8 @@ const extractTranslatableContent = (content, currentPath = [], state = {
16
16
  varIndex++;
17
17
  return placeholder;
18
18
  });
19
- const contentWithoutPlaceholders = modifiedContent.replace(/<\\d+>/g, "");
20
- if (/[\\p{L}\\p{N}]/u.test(contentWithoutPlaceholders)) {
19
+ const contentWithoutPlaceholders = modifiedContent.replace(/<\d+>/g, "");
20
+ if (/[\p{L}\p{N}]/u.test(contentWithoutPlaceholders)) {
21
21
  state.extractedContent.push({
22
22
  index: state.currentIndex,
23
23
  path: currentPath,
@@ -1 +1 @@
1
- {"version":3,"file":"extractTranslatableContent.cjs","names":[],"sources":["../../../src/fill/extractTranslatableContent.ts"],"sourcesContent":["export type ExtractedContentProps = {\n index: number;\n path: (string | number)[];\n value: string;\n replacement?: Record<string, string>;\n};\n\nexport type ExtractedContentResult = {\n extractedContent: ExtractedContentProps[];\n translatableDictionary: Record<number, string>;\n};\n\nexport const extractTranslatableContent = (\n content: any,\n currentPath: (string | number)[] = [],\n state = {\n currentIndex: 1,\n extractedContent: [] as ExtractedContentProps[],\n translatableDictionary: {} as Record<number, string>,\n }\n): ExtractedContentResult => {\n if (typeof content === 'string') {\n const regex = /[{]+.*?[}]+/g;\n const replacement: Record<string, string> = {};\n let varIndex = 1;\n\n const modifiedContent = content.replace(regex, (matchStr) => {\n const placeholder = `<${varIndex}>`;\n replacement[placeholder] = matchStr;\n varIndex++;\n return placeholder;\n });\n\n // Only extract strings that contain at least one letter or number outside of placeholders.\n // This avoids extracting strings that are only spaces, special characters, or just variables.\n const contentWithoutPlaceholders = modifiedContent.replace(/<\\\\d+>/g, '');\n if (/[\\\\p{L}\\\\p{N}]/u.test(contentWithoutPlaceholders)) {\n state.extractedContent.push({\n index: state.currentIndex,\n path: currentPath,\n value: modifiedContent,\n replacement:\n Object.keys(replacement).length > 0 ? replacement : undefined,\n });\n state.translatableDictionary[state.currentIndex] = modifiedContent;\n state.currentIndex++;\n }\n } else if (Array.isArray(content)) {\n content.forEach((item, index) => {\n extractTranslatableContent(item, [...currentPath, index], state);\n });\n } else if (typeof content === 'object' && content !== null) {\n for (const key in content) {\n if (Object.hasOwn(content, key)) {\n extractTranslatableContent(content[key], [...currentPath, key], state);\n }\n }\n }\n\n return {\n extractedContent: state.extractedContent,\n translatableDictionary: state.translatableDictionary,\n };\n};\n\nexport const reinsertTranslatedContent = (\n baseContent: any,\n extractedContentProps: ExtractedContentProps[],\n translatedDictionary: Record<number, string>\n): any => {\n const result = structuredClone(baseContent);\n\n for (const { index, path, replacement } of extractedContentProps) {\n let translatedValue = translatedDictionary[index];\n\n if (translatedValue !== undefined) {\n if (replacement) {\n for (const [placeholder, originalVar] of Object.entries(replacement)) {\n translatedValue = translatedValue.replace(placeholder, originalVar);\n }\n }\n\n let current = result;\n for (let i = 0; i < path.length - 1; i++) {\n current = current[path[i]];\n }\n current[path[path.length - 1]] = translatedValue;\n }\n }\n\n return result;\n};\n"],"mappings":";;;AAYA,MAAa,8BACX,SACA,cAAmC,EAAE,EACrC,QAAQ;CACN,cAAc;CACd,kBAAkB,EAAE;CACpB,wBAAwB,EAAE;CAC3B,KAC0B;AAC3B,KAAI,OAAO,YAAY,UAAU;EAC/B,MAAM,QAAQ;EACd,MAAM,cAAsC,EAAE;EAC9C,IAAI,WAAW;EAEf,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ,aAAa;GAC3D,MAAM,cAAc,IAAI,SAAS;AACjC,eAAY,eAAe;AAC3B;AACA,UAAO;IACP;EAIF,MAAM,6BAA6B,gBAAgB,QAAQ,WAAW,GAAG;AACzE,MAAI,kBAAkB,KAAK,2BAA2B,EAAE;AACtD,SAAM,iBAAiB,KAAK;IAC1B,OAAO,MAAM;IACb,MAAM;IACN,OAAO;IACP,aACE,OAAO,KAAK,YAAY,CAAC,SAAS,IAAI,cAAc;IACvD,CAAC;AACF,SAAM,uBAAuB,MAAM,gBAAgB;AACnD,SAAM;;YAEC,MAAM,QAAQ,QAAQ,CAC/B,SAAQ,SAAS,MAAM,UAAU;AAC/B,6BAA2B,MAAM,CAAC,GAAG,aAAa,MAAM,EAAE,MAAM;GAChE;UACO,OAAO,YAAY,YAAY,YAAY,MACpD;OAAK,MAAM,OAAO,QAChB,KAAI,OAAO,OAAO,SAAS,IAAI,CAC7B,4BAA2B,QAAQ,MAAM,CAAC,GAAG,aAAa,IAAI,EAAE,MAAM;;AAK5E,QAAO;EACL,kBAAkB,MAAM;EACxB,wBAAwB,MAAM;EAC/B;;AAGH,MAAa,6BACX,aACA,uBACA,yBACQ;CACR,MAAM,SAAS,gBAAgB,YAAY;AAE3C,MAAK,MAAM,EAAE,OAAO,MAAM,iBAAiB,uBAAuB;EAChE,IAAI,kBAAkB,qBAAqB;AAE3C,MAAI,oBAAoB,QAAW;AACjC,OAAI,YACF,MAAK,MAAM,CAAC,aAAa,gBAAgB,OAAO,QAAQ,YAAY,CAClE,mBAAkB,gBAAgB,QAAQ,aAAa,YAAY;GAIvE,IAAI,UAAU;AACd,QAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,IACnC,WAAU,QAAQ,KAAK;AAEzB,WAAQ,KAAK,KAAK,SAAS,MAAM;;;AAIrC,QAAO"}
1
+ {"version":3,"file":"extractTranslatableContent.cjs","names":[],"sources":["../../../src/fill/extractTranslatableContent.ts"],"sourcesContent":["export type ExtractedContentProps = {\n index: number;\n path: (string | number)[];\n value: string;\n replacement?: Record<string, string>;\n};\n\nexport type ExtractedContentResult = {\n extractedContent: ExtractedContentProps[];\n translatableDictionary: Record<number, string>;\n};\n\nexport const extractTranslatableContent = (\n content: any,\n currentPath: (string | number)[] = [],\n state = {\n currentIndex: 1,\n extractedContent: [] as ExtractedContentProps[],\n translatableDictionary: {} as Record<number, string>,\n }\n): ExtractedContentResult => {\n if (typeof content === 'string') {\n const regex = /[{]+.*?[}]+/g;\n const replacement: Record<string, string> = {};\n let varIndex = 1;\n\n const modifiedContent = content.replace(regex, (matchStr) => {\n const placeholder = `<${varIndex}>`;\n replacement[placeholder] = matchStr;\n varIndex++;\n return placeholder;\n });\n\n // Only extract strings that contain at least one letter or number outside of placeholders.\n // This avoids extracting strings that are only spaces, special characters, or just variables.\n const contentWithoutPlaceholders = modifiedContent.replace(/<\\d+>/g, '');\n if (/[\\p{L}\\p{N}]/u.test(contentWithoutPlaceholders)) {\n state.extractedContent.push({\n index: state.currentIndex,\n path: currentPath,\n value: modifiedContent,\n replacement:\n Object.keys(replacement).length > 0 ? replacement : undefined,\n });\n state.translatableDictionary[state.currentIndex] = modifiedContent;\n state.currentIndex++;\n }\n } else if (Array.isArray(content)) {\n content.forEach((item, index) => {\n extractTranslatableContent(item, [...currentPath, index], state);\n });\n } else if (typeof content === 'object' && content !== null) {\n for (const key in content) {\n if (Object.hasOwn(content, key)) {\n extractTranslatableContent(content[key], [...currentPath, key], state);\n }\n }\n }\n\n return {\n extractedContent: state.extractedContent,\n translatableDictionary: state.translatableDictionary,\n };\n};\n\nexport const reinsertTranslatedContent = (\n baseContent: any,\n extractedContentProps: ExtractedContentProps[],\n translatedDictionary: Record<number, string>\n): any => {\n const result = structuredClone(baseContent);\n\n for (const { index, path, replacement } of extractedContentProps) {\n let translatedValue = translatedDictionary[index];\n\n if (translatedValue !== undefined) {\n if (replacement) {\n for (const [placeholder, originalVar] of Object.entries(replacement)) {\n translatedValue = translatedValue.replace(placeholder, originalVar);\n }\n }\n\n let current = result;\n for (let i = 0; i < path.length - 1; i++) {\n current = current[path[i]];\n }\n current[path[path.length - 1]] = translatedValue;\n }\n }\n\n return result;\n};\n"],"mappings":";;;AAYA,MAAa,8BACX,SACA,cAAmC,EAAE,EACrC,QAAQ;CACN,cAAc;CACd,kBAAkB,EAAE;CACpB,wBAAwB,EAAE;CAC3B,KAC0B;AAC3B,KAAI,OAAO,YAAY,UAAU;EAC/B,MAAM,QAAQ;EACd,MAAM,cAAsC,EAAE;EAC9C,IAAI,WAAW;EAEf,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ,aAAa;GAC3D,MAAM,cAAc,IAAI,SAAS;AACjC,eAAY,eAAe;AAC3B;AACA,UAAO;IACP;EAIF,MAAM,6BAA6B,gBAAgB,QAAQ,UAAU,GAAG;AACxE,MAAI,gBAAgB,KAAK,2BAA2B,EAAE;AACpD,SAAM,iBAAiB,KAAK;IAC1B,OAAO,MAAM;IACb,MAAM;IACN,OAAO;IACP,aACE,OAAO,KAAK,YAAY,CAAC,SAAS,IAAI,cAAc;IACvD,CAAC;AACF,SAAM,uBAAuB,MAAM,gBAAgB;AACnD,SAAM;;YAEC,MAAM,QAAQ,QAAQ,CAC/B,SAAQ,SAAS,MAAM,UAAU;AAC/B,6BAA2B,MAAM,CAAC,GAAG,aAAa,MAAM,EAAE,MAAM;GAChE;UACO,OAAO,YAAY,YAAY,YAAY,MACpD;OAAK,MAAM,OAAO,QAChB,KAAI,OAAO,OAAO,SAAS,IAAI,CAC7B,4BAA2B,QAAQ,MAAM,CAAC,GAAG,aAAa,IAAI,EAAE,MAAM;;AAK5E,QAAO;EACL,kBAAkB,MAAM;EACxB,wBAAwB,MAAM;EAC/B;;AAGH,MAAa,6BACX,aACA,uBACA,yBACQ;CACR,MAAM,SAAS,gBAAgB,YAAY;AAE3C,MAAK,MAAM,EAAE,OAAO,MAAM,iBAAiB,uBAAuB;EAChE,IAAI,kBAAkB,qBAAqB;AAE3C,MAAI,oBAAoB,QAAW;AACjC,OAAI,YACF,MAAK,MAAM,CAAC,aAAa,gBAAgB,OAAO,QAAQ,YAAY,CAClE,mBAAkB,gBAAgB,QAAQ,aAAa,YAAY;GAIvE,IAAI,UAAU;AACd,QAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,IACnC,WAAU,QAAQ,KAAK;AAEzB,WAAQ,KAAK,KAAK,SAAS,MAAM;;;AAIrC,QAAO"}
@@ -16,7 +16,7 @@ let _intlayer_config_node = require("@intlayer/config/node");
16
16
  let _intlayer_ai = require("@intlayer/ai");
17
17
 
18
18
  //#region src/fill/fill.ts
19
- const NB_CONCURRENT_TRANSLATIONS = 1;
19
+ const NB_CONCURRENT_TRANSLATIONS = 7;
20
20
  /**
21
21
  * Fill translations based on the provided options.
22
22
  */
@@ -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';\nimport { checkAISDKAccess } from '@intlayer/ai';\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 = 1;\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 } = aiResult;\n\n const { hasAIAccess, error } = await checkAISDKAccess(aiConfig!);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\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';\nimport { checkAISDKAccess } from '@intlayer/ai';\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 } = aiResult;\n\n const { hasAIAccess, error } = await checkAISDKAccess(aiConfig!);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\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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"}
@@ -2,7 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
3
3
  const require_fill_deepMergeContent = require('./deepMergeContent.cjs');
4
4
  const require_fill_extractTranslatableContent = require('./extractTranslatableContent.cjs');
5
- const require_fill_getFilterMissingContentPerLocale = require('./getFilterMissingContentPerLocale.cjs');
6
5
  let node_path = require("node:path");
7
6
  let _intlayer_chokidar_utils = require("@intlayer/chokidar/utils");
8
7
  let _intlayer_config_colors = require("@intlayer/config/colors");
@@ -91,41 +90,42 @@ const translateDictionary = async (task, configuration, options) => {
91
90
  let targetLocaleDictionary;
92
91
  if (typeof baseUnmergedDictionary.locale === "string") {
93
92
  const targetLocaleFilePath = baseUnmergedDictionary.filePath?.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`);
94
- const targetUnmergedDictionary = targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0;
95
- targetLocaleDictionary = targetUnmergedDictionary ?? {
93
+ targetLocaleDictionary = (targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0) ?? {
96
94
  key: baseUnmergedDictionary.key,
97
95
  content: {},
98
96
  filePath: targetLocaleFilePath,
99
97
  locale: targetLocale
100
98
  };
101
- if (mode === "complete") dictionaryToProcess = require_fill_getFilterMissingContentPerLocale.getFilterMissingContentPerLocale(dictionaryToProcess, targetUnmergedDictionary);
102
99
  } else {
103
100
  if (mode === "complete") dictionaryToProcess = (0, _intlayer_core_plugins.getFilterMissingTranslationsDictionary)(dictionaryToProcess, targetLocale);
104
101
  dictionaryToProcess = (0, _intlayer_core_plugins.getPerLocaleDictionary)(dictionaryToProcess, task.sourceLocale);
105
102
  targetLocaleDictionary = (0, _intlayer_core_plugins.getPerLocaleDictionary)(baseUnmergedDictionary, targetLocale);
106
103
  }
104
+ if (mode === "complete") dictionaryToProcess = {
105
+ ...dictionaryToProcess,
106
+ content: (0, _intlayer_chokidar_utils.excludeObjectFormat)(dictionaryToProcess.content, targetLocaleDictionary.content) ?? {}
107
+ };
107
108
  const localePreset = (0, _intlayer_config_logger.colon)([
108
109
  (0, _intlayer_config_logger.colorize)("[", _intlayer_config_colors.GREY_DARK),
109
110
  (0, _intlayer_chokidar_utils.formatLocale)(targetLocale),
110
111
  (0, _intlayer_config_logger.colorize)("]", _intlayer_config_colors.GREY_DARK)
111
112
  ].join(""), { colSize: 18 });
112
113
  appLogger(`${task.dictionaryPreset}${localePreset} Preparing ${(0, _intlayer_config_logger.colorizePath)((0, node_path.basename)(targetLocaleDictionary.filePath))}`, { level: "info" });
113
- const { extractedContent, translatableDictionary } = require_fill_extractTranslatableContent.extractTranslatableContent(dictionaryToProcess.content);
114
- const chunkedJsonContent = (0, _intlayer_chokidar_utils.chunkJSON)(translatableDictionary, CHUNK_SIZE);
114
+ const chunkedJsonContent = (0, _intlayer_chokidar_utils.chunkJSON)(dictionaryToProcess.content, CHUNK_SIZE);
115
115
  const nbOfChunks = chunkedJsonContent.length;
116
116
  if (nbOfChunks > 1) appLogger(`${task.dictionaryPreset}${localePreset} Split into ${(0, _intlayer_config_logger.colorizeNumber)(nbOfChunks)} chunks for translation`, { level: "info" });
117
117
  const chunkResult = [];
118
118
  const chunkPromises = chunkedJsonContent.map(async (chunk) => {
119
119
  const chunkPreset = createChunkPreset(chunk.index, chunk.total);
120
120
  if (nbOfChunks > 1) appLogger(`${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`, { level: "info" });
121
- const chunkContent = (0, _intlayer_chokidar_utils.reconstructFromSingleChunk)(chunk);
122
- const presetOutputContent = (0, _intlayer_chokidar_utils.reduceObjectFormat)(translatableDictionary, chunkContent);
121
+ const reconstructedChunk = (0, _intlayer_chokidar_utils.reconstructFromSingleChunk)(chunk);
122
+ const { extractedContent: chunkExtractedContent, translatableDictionary: chunkTranslatableDictionary } = require_fill_extractTranslatableContent.extractTranslatableContent(reconstructedChunk);
123
123
  const executeTranslation = async () => {
124
124
  return await (0, _intlayer_config_utils.retryManager)(async () => {
125
125
  let translationResult;
126
126
  if (aiClient && aiConfig) translationResult = await aiClient.translateJSON({
127
- entryFileContent: chunkContent,
128
- presetOutputContent,
127
+ entryFileContent: chunkTranslatableDictionary,
128
+ presetOutputContent: chunkTranslatableDictionary,
129
129
  dictionaryDescription: dictionaryToProcess.description ?? metadata?.description ?? "",
130
130
  entryLocale: task.sourceLocale,
131
131
  outputLocale: targetLocale,
@@ -133,8 +133,8 @@ const translateDictionary = async (task, configuration, options) => {
133
133
  aiConfig
134
134
  });
135
135
  else translationResult = await intlayerAPI.ai.translateJSON({
136
- entryFileContent: chunkContent,
137
- presetOutputContent,
136
+ entryFileContent: chunkTranslatableDictionary,
137
+ presetOutputContent: chunkTranslatableDictionary,
138
138
  dictionaryDescription: dictionaryToProcess.description ?? metadata?.description ?? "",
139
139
  entryLocale: task.sourceLocale,
140
140
  outputLocale: targetLocale,
@@ -142,10 +142,10 @@ const translateDictionary = async (task, configuration, options) => {
142
142
  aiOptions
143
143
  }).then((result) => result.data);
144
144
  if (!translationResult?.fileContent) throw new Error("No content result");
145
- const { isIdentic, error } = (0, _intlayer_chokidar_utils.verifyIdenticObjectFormat)(translationResult.fileContent, chunkContent);
145
+ const { isIdentic, error } = (0, _intlayer_chokidar_utils.verifyIdenticObjectFormat)(translationResult.fileContent, chunkTranslatableDictionary);
146
146
  if (!isIdentic) throw new Error(`Translation result does not match expected format: ${error}`);
147
147
  notifySuccess();
148
- return translationResult.fileContent;
148
+ return require_fill_extractTranslatableContent.reinsertTranslatedContent(reconstructedChunk, chunkExtractedContent, translationResult.fileContent);
149
149
  }, {
150
150
  maxRetry: MAX_RETRY,
151
151
  delay: RETRY_DELAY,
@@ -168,14 +168,12 @@ const translateDictionary = async (task, configuration, options) => {
168
168
  (await Promise.all(chunkPromises)).sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index).forEach(({ result }) => {
169
169
  chunkResult.push(result);
170
170
  });
171
- const mergedTranslatedDictionary = (0, _intlayer_chokidar_utils.mergeChunks)(chunkResult);
172
- const reinsertedContent = require_fill_extractTranslatableContent.reinsertTranslatedContent(dictionaryToProcess.content, extractedContent, mergedTranslatedDictionary);
173
- let finalContent = {
171
+ const reinsertedContent = (0, _intlayer_chokidar_utils.mergeChunks)(chunkResult);
172
+ const merged = {
174
173
  ...dictionaryToProcess,
175
174
  content: reinsertedContent
176
- }.content;
177
- if (typeof baseUnmergedDictionary.locale === "string") finalContent = require_fill_deepMergeContent.deepMergeContent(targetLocaleDictionary.content ?? {}, finalContent);
178
- return [targetLocale, finalContent];
175
+ };
176
+ return [targetLocale, require_fill_deepMergeContent.deepMergeContent(targetLocaleDictionary.content ?? {}, merged.content)];
179
177
  }));
180
178
  const translatedContent = Object.fromEntries(translatedContentResults);
181
179
  let dictionaryOutput = {
@@ -1 +1 @@
1
- {"version":3,"file":"translateDictionary.cjs","names":["ANSIColors","getFilterMissingContentPerLocale","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 formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n reduceObjectFormat,\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 { getFilterMissingContentPerLocale } from './getFilterMissingContentPerLocale';\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\n // In complete mode, filter out already translated content\n if (mode === 'complete') {\n dictionaryToProcess = getFilterMissingContentPerLocale(\n dictionaryToProcess,\n targetUnmergedDictionary\n );\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 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 { extractedContent, translatableDictionary } =\n extractTranslatableContent(dictionaryToProcess.content);\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n translatableDictionary 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 chunkContent = reconstructFromSingleChunk(chunk);\n const presetOutputContent = reduceObjectFormat(\n translatableDictionary,\n chunkContent\n ) as unknown as JSON;\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: chunkContent as unknown as JSON,\n presetOutputContent,\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: chunkContent as unknown as JSON,\n presetOutputContent,\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 chunkContent\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 translationResult.fileContent;\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 partial JSON objects produced from each chunk into a single object\n const mergedTranslatedDictionary = mergeChunks(chunkResult);\n\n const reinsertedContent = reinsertTranslatedContent(\n dictionaryToProcess.content,\n extractedContent,\n mergedTranslatedDictionary as Record<number, string>\n );\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // For per-locale files, merge the newly translated content with existing target content\n let finalContent = merged.content;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // Deep merge: existing content + newly translated content\n finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n finalContent\n );\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 = 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 +1 @@
1
- {"version":3,"file":"IntlayerEventListener.mjs","names":[],"sources":["../../src/IntlayerEventListener.ts"],"sourcesContent":["import { getIntlayerAPIProxy } from '@intlayer/api';\n// @ts-ignore: @intlayer/backend is not built yet\nimport type { DictionaryAPI, MessageEventData } from '@intlayer/backend';\nimport configuration from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { EventSource } from 'eventsource';\n\nexport type IntlayerMessageEvent = MessageEvent;\n\n/**\n * IntlayerEventListener class to listen for dictionary changes via SSE (Server-Sent Events).\n *\n * Usage example:\n *\n * import { buildIntlayerDictionary } from './transpiler/declaration_file_to_dictionary/intlayer_dictionary';\n * import { IntlayerEventListener } from '@intlayer/api';\n *\n * export const checkDictionaryChanges = async () => {\n * // Instantiate the listener\n * const eventListener = new IntlayerEventListener();\n *\n * // Set up your callbacks\n * eventListener.onDictionaryChange = async (dictionary) => {\n * await buildIntlayerDictionary(dictionary);\n * };\n *\n * // Initialize the listener\n * await eventListener.initialize();\n *\n * // Optionally, clean up later when you’re done\n * // eventListener.cleanup();\n * };\n */\nexport class IntlayerEventListener {\n private appLogger = getAppLogger(configuration);\n\n private eventSource: EventSource | null = null;\n private reconnectAttempts = 0;\n private maxReconnectAttempts = 5;\n private reconnectDelay = 1000; // Start with 1 second\n private isManuallyDisconnected = false;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Callback triggered when a Dictionary is ADDED.\n */\n public onDictionaryAdded?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is UPDATED.\n */\n public onDictionaryChange?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is DELETED.\n */\n public onDictionaryDeleted?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when connection is established or re-established.\n */\n public onConnectionOpen?: () => any;\n\n /**\n * Callback triggered when connection encounters an error.\n */\n public onConnectionError?: (error: Event) => any;\n\n constructor(private intlayerConfig: IntlayerConfig = configuration) {\n this.appLogger = getAppLogger(this.intlayerConfig);\n }\n\n /**\n * Initializes the EventSource connection using the given intlayerConfig\n * (or the default config if none was provided).\n */\n public async initialize(): Promise<void> {\n this.isManuallyDisconnected = false;\n await this.connect();\n }\n\n /**\n * Establishes the EventSource connection with automatic reconnection support.\n */\n private async connect(): Promise<void> {\n try {\n const backendURL = this.intlayerConfig.editor.backendURL;\n\n // Retrieve the access token via proxy\n const accessToken = await getIntlayerAPIProxy(\n undefined,\n this.intlayerConfig\n ).oAuth.getOAuth2AccessToken();\n\n if (!accessToken) {\n throw new Error('Failed to retrieve access token');\n }\n\n const API_ROUTE = `${backendURL}/api/event-listener`;\n\n // Close existing connection if any\n if (this.eventSource) {\n this.eventSource.close();\n }\n\n this.eventSource = new EventSource(API_ROUTE, {\n fetch: (input, init) =>\n fetch(input, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n Authorization: `Bearer ${accessToken.data?.accessToken}`,\n },\n }),\n });\n\n this.eventSource.onopen = () => {\n this.reconnectAttempts = 0;\n this.reconnectDelay = 1000; // Reset delay\n this.onConnectionOpen?.();\n };\n\n this.eventSource.onmessage = (event) => this.handleMessage(event);\n this.eventSource.onerror = (event) => this.handleError(event);\n } catch (_error) {\n this.appLogger('Failed to establish EventSource connection:', {\n level: 'error',\n });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Cleans up (closes) the EventSource connection.\n */\n public cleanup(): void {\n this.isManuallyDisconnected = true;\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n }\n\n /**\n * Schedules a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(): void {\n if (\n this.isManuallyDisconnected ||\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n this.appLogger(\n [\n `Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`,\n ],\n {\n level: 'error',\n }\n );\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = this.reconnectDelay * 2 ** (this.reconnectAttempts - 1); // Exponential backoff\n\n this.appLogger(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`\n );\n\n this.reconnectTimeout = setTimeout(async () => {\n if (!this.isManuallyDisconnected) {\n await this.connect();\n }\n }, delay);\n }\n\n /**\n * Handles incoming SSE messages, parses the event data,\n * and invokes the appropriate callback.\n */\n private async handleMessage(event: IntlayerMessageEvent): Promise<void> {\n try {\n const { data } = event;\n\n const dataJSON: MessageEventData[] = JSON.parse(data);\n\n for (const dataEl of dataJSON) {\n switch (dataEl.object) {\n case 'DICTIONARY':\n switch (dataEl.status) {\n case 'ADDED':\n await this.onDictionaryAdded?.(dataEl.data);\n break;\n case 'UPDATED':\n await this.onDictionaryChange?.(dataEl.data);\n break;\n case 'DELETED':\n await this.onDictionaryDeleted?.(dataEl.data);\n break;\n default:\n this.appLogger(\n ['Unhandled dictionary status:', dataEl.status],\n {\n level: 'error',\n }\n );\n break;\n }\n break;\n default:\n this.appLogger(['Unknown object type:', dataEl.object], {\n level: 'error',\n });\n break;\n }\n }\n } catch (error) {\n this.appLogger(['Error processing dictionary update:', error], {\n level: 'error',\n });\n }\n }\n\n /**\n * Handles any SSE errors and attempts reconnection if appropriate.\n */\n private handleError(event: Event): void {\n const errorEvent = event as any;\n\n // Log detailed error information\n this.appLogger(\n [\n 'EventSource error:',\n {\n type: errorEvent.type,\n message: errorEvent.message,\n code: errorEvent.code,\n readyState: this.eventSource?.readyState,\n url: this.eventSource?.url,\n },\n ],\n {\n level: 'error',\n }\n );\n\n // Notify error callback\n this.onConnectionError?.(event);\n\n // Check if this is a connection close error\n const isConnectionClosed =\n errorEvent.type === 'error' &&\n (errorEvent.message?.includes('terminated') ||\n errorEvent.message?.includes('closed') ||\n this.eventSource?.readyState === EventSource.CLOSED);\n\n if (isConnectionClosed && !this.isManuallyDisconnected) {\n this.appLogger(\n 'Connection was terminated by server, attempting to reconnect...'\n );\n this.scheduleReconnect();\n } else {\n // For other types of errors, close the connection\n this.cleanup();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"IntlayerEventListener.mjs","names":[],"sources":["../../src/IntlayerEventListener.ts"],"sourcesContent":["import { getIntlayerAPIProxy } from '@intlayer/api';\n// @ts-ignore: @intlayer/backend is not built yet\nimport type { DictionaryAPI, MessageEventData } from '@intlayer/backend';\nimport { default as configuration } from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { EventSource } from 'eventsource';\n\nexport type IntlayerMessageEvent = MessageEvent;\n\n/**\n * IntlayerEventListener class to listen for dictionary changes via SSE (Server-Sent Events).\n *\n * Usage example:\n *\n * import { buildIntlayerDictionary } from './transpiler/declaration_file_to_dictionary/intlayer_dictionary';\n * import { IntlayerEventListener } from '@intlayer/api';\n *\n * export const checkDictionaryChanges = async () => {\n * // Instantiate the listener\n * const eventListener = new IntlayerEventListener();\n *\n * // Set up your callbacks\n * eventListener.onDictionaryChange = async (dictionary) => {\n * await buildIntlayerDictionary(dictionary);\n * };\n *\n * // Initialize the listener\n * await eventListener.initialize();\n *\n * // Optionally, clean up later when you’re done\n * // eventListener.cleanup();\n * };\n */\nexport class IntlayerEventListener {\n private appLogger = getAppLogger(configuration);\n\n private eventSource: EventSource | null = null;\n private reconnectAttempts = 0;\n private maxReconnectAttempts = 5;\n private reconnectDelay = 1000; // Start with 1 second\n private isManuallyDisconnected = false;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Callback triggered when a Dictionary is ADDED.\n */\n public onDictionaryAdded?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is UPDATED.\n */\n public onDictionaryChange?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when a Dictionary is DELETED.\n */\n public onDictionaryDeleted?: (dictionary: DictionaryAPI) => any;\n\n /**\n * Callback triggered when connection is established or re-established.\n */\n public onConnectionOpen?: () => any;\n\n /**\n * Callback triggered when connection encounters an error.\n */\n public onConnectionError?: (error: Event) => any;\n\n constructor(private intlayerConfig: IntlayerConfig = configuration) {\n this.appLogger = getAppLogger(this.intlayerConfig);\n }\n\n /**\n * Initializes the EventSource connection using the given intlayerConfig\n * (or the default config if none was provided).\n */\n public async initialize(): Promise<void> {\n this.isManuallyDisconnected = false;\n await this.connect();\n }\n\n /**\n * Establishes the EventSource connection with automatic reconnection support.\n */\n private async connect(): Promise<void> {\n try {\n const backendURL = this.intlayerConfig.editor.backendURL;\n\n // Retrieve the access token via proxy\n const accessToken = await getIntlayerAPIProxy(\n undefined,\n this.intlayerConfig\n ).oAuth.getOAuth2AccessToken();\n\n if (!accessToken) {\n throw new Error('Failed to retrieve access token');\n }\n\n const API_ROUTE = `${backendURL}/api/event-listener`;\n\n // Close existing connection if any\n if (this.eventSource) {\n this.eventSource.close();\n }\n\n this.eventSource = new EventSource(API_ROUTE, {\n fetch: (input, init) =>\n fetch(input, {\n ...init,\n headers: {\n ...(init?.headers ?? {}),\n Authorization: `Bearer ${accessToken.data?.accessToken}`,\n },\n }),\n });\n\n this.eventSource.onopen = () => {\n this.reconnectAttempts = 0;\n this.reconnectDelay = 1000; // Reset delay\n this.onConnectionOpen?.();\n };\n\n this.eventSource.onmessage = (event) => this.handleMessage(event);\n this.eventSource.onerror = (event) => this.handleError(event);\n } catch (_error) {\n this.appLogger('Failed to establish EventSource connection:', {\n level: 'error',\n });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Cleans up (closes) the EventSource connection.\n */\n public cleanup(): void {\n this.isManuallyDisconnected = true;\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n }\n\n /**\n * Schedules a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(): void {\n if (\n this.isManuallyDisconnected ||\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n this.appLogger(\n [\n `Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`,\n ],\n {\n level: 'error',\n }\n );\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = this.reconnectDelay * 2 ** (this.reconnectAttempts - 1); // Exponential backoff\n\n this.appLogger(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`\n );\n\n this.reconnectTimeout = setTimeout(async () => {\n if (!this.isManuallyDisconnected) {\n await this.connect();\n }\n }, delay);\n }\n\n /**\n * Handles incoming SSE messages, parses the event data,\n * and invokes the appropriate callback.\n */\n private async handleMessage(event: IntlayerMessageEvent): Promise<void> {\n try {\n const { data } = event;\n\n const dataJSON: MessageEventData[] = JSON.parse(data);\n\n for (const dataEl of dataJSON) {\n switch (dataEl.object) {\n case 'DICTIONARY':\n switch (dataEl.status) {\n case 'ADDED':\n await this.onDictionaryAdded?.(dataEl.data);\n break;\n case 'UPDATED':\n await this.onDictionaryChange?.(dataEl.data);\n break;\n case 'DELETED':\n await this.onDictionaryDeleted?.(dataEl.data);\n break;\n default:\n this.appLogger(\n ['Unhandled dictionary status:', dataEl.status],\n {\n level: 'error',\n }\n );\n break;\n }\n break;\n default:\n this.appLogger(['Unknown object type:', dataEl.object], {\n level: 'error',\n });\n break;\n }\n }\n } catch (error) {\n this.appLogger(['Error processing dictionary update:', error], {\n level: 'error',\n });\n }\n }\n\n /**\n * Handles any SSE errors and attempts reconnection if appropriate.\n */\n private handleError(event: Event): void {\n const errorEvent = event as any;\n\n // Log detailed error information\n this.appLogger(\n [\n 'EventSource error:',\n {\n type: errorEvent.type,\n message: errorEvent.message,\n code: errorEvent.code,\n readyState: this.eventSource?.readyState,\n url: this.eventSource?.url,\n },\n ],\n {\n level: 'error',\n }\n );\n\n // Notify error callback\n this.onConnectionError?.(event);\n\n // Check if this is a connection close error\n const isConnectionClosed =\n errorEvent.type === 'error' &&\n (errorEvent.message?.includes('terminated') ||\n errorEvent.message?.includes('closed') ||\n this.eventSource?.readyState === EventSource.CLOSED);\n\n if (isConnectionClosed && !this.isManuallyDisconnected) {\n this.appLogger(\n 'Connection was terminated by server, attempting to reconnect...'\n );\n this.scheduleReconnect();\n } else {\n // For other types of errors, close the connection\n this.cleanup();\n }\n }\n}\n"],"mappings":"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"}
@@ -14,8 +14,8 @@ const extractTranslatableContent = (content, currentPath = [], state = {
14
14
  varIndex++;
15
15
  return placeholder;
16
16
  });
17
- const contentWithoutPlaceholders = modifiedContent.replace(/<\\d+>/g, "");
18
- if (/[\\p{L}\\p{N}]/u.test(contentWithoutPlaceholders)) {
17
+ const contentWithoutPlaceholders = modifiedContent.replace(/<\d+>/g, "");
18
+ if (/[\p{L}\p{N}]/u.test(contentWithoutPlaceholders)) {
19
19
  state.extractedContent.push({
20
20
  index: state.currentIndex,
21
21
  path: currentPath,
@@ -1 +1 @@
1
- {"version":3,"file":"extractTranslatableContent.mjs","names":[],"sources":["../../../src/fill/extractTranslatableContent.ts"],"sourcesContent":["export type ExtractedContentProps = {\n index: number;\n path: (string | number)[];\n value: string;\n replacement?: Record<string, string>;\n};\n\nexport type ExtractedContentResult = {\n extractedContent: ExtractedContentProps[];\n translatableDictionary: Record<number, string>;\n};\n\nexport const extractTranslatableContent = (\n content: any,\n currentPath: (string | number)[] = [],\n state = {\n currentIndex: 1,\n extractedContent: [] as ExtractedContentProps[],\n translatableDictionary: {} as Record<number, string>,\n }\n): ExtractedContentResult => {\n if (typeof content === 'string') {\n const regex = /[{]+.*?[}]+/g;\n const replacement: Record<string, string> = {};\n let varIndex = 1;\n\n const modifiedContent = content.replace(regex, (matchStr) => {\n const placeholder = `<${varIndex}>`;\n replacement[placeholder] = matchStr;\n varIndex++;\n return placeholder;\n });\n\n // Only extract strings that contain at least one letter or number outside of placeholders.\n // This avoids extracting strings that are only spaces, special characters, or just variables.\n const contentWithoutPlaceholders = modifiedContent.replace(/<\\\\d+>/g, '');\n if (/[\\\\p{L}\\\\p{N}]/u.test(contentWithoutPlaceholders)) {\n state.extractedContent.push({\n index: state.currentIndex,\n path: currentPath,\n value: modifiedContent,\n replacement:\n Object.keys(replacement).length > 0 ? replacement : undefined,\n });\n state.translatableDictionary[state.currentIndex] = modifiedContent;\n state.currentIndex++;\n }\n } else if (Array.isArray(content)) {\n content.forEach((item, index) => {\n extractTranslatableContent(item, [...currentPath, index], state);\n });\n } else if (typeof content === 'object' && content !== null) {\n for (const key in content) {\n if (Object.hasOwn(content, key)) {\n extractTranslatableContent(content[key], [...currentPath, key], state);\n }\n }\n }\n\n return {\n extractedContent: state.extractedContent,\n translatableDictionary: state.translatableDictionary,\n };\n};\n\nexport const reinsertTranslatedContent = (\n baseContent: any,\n extractedContentProps: ExtractedContentProps[],\n translatedDictionary: Record<number, string>\n): any => {\n const result = structuredClone(baseContent);\n\n for (const { index, path, replacement } of extractedContentProps) {\n let translatedValue = translatedDictionary[index];\n\n if (translatedValue !== undefined) {\n if (replacement) {\n for (const [placeholder, originalVar] of Object.entries(replacement)) {\n translatedValue = translatedValue.replace(placeholder, originalVar);\n }\n }\n\n let current = result;\n for (let i = 0; i < path.length - 1; i++) {\n current = current[path[i]];\n }\n current[path[path.length - 1]] = translatedValue;\n }\n }\n\n return result;\n};\n"],"mappings":";AAYA,MAAa,8BACX,SACA,cAAmC,EAAE,EACrC,QAAQ;CACN,cAAc;CACd,kBAAkB,EAAE;CACpB,wBAAwB,EAAE;CAC3B,KAC0B;AAC3B,KAAI,OAAO,YAAY,UAAU;EAC/B,MAAM,QAAQ;EACd,MAAM,cAAsC,EAAE;EAC9C,IAAI,WAAW;EAEf,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ,aAAa;GAC3D,MAAM,cAAc,IAAI,SAAS;AACjC,eAAY,eAAe;AAC3B;AACA,UAAO;IACP;EAIF,MAAM,6BAA6B,gBAAgB,QAAQ,WAAW,GAAG;AACzE,MAAI,kBAAkB,KAAK,2BAA2B,EAAE;AACtD,SAAM,iBAAiB,KAAK;IAC1B,OAAO,MAAM;IACb,MAAM;IACN,OAAO;IACP,aACE,OAAO,KAAK,YAAY,CAAC,SAAS,IAAI,cAAc;IACvD,CAAC;AACF,SAAM,uBAAuB,MAAM,gBAAgB;AACnD,SAAM;;YAEC,MAAM,QAAQ,QAAQ,CAC/B,SAAQ,SAAS,MAAM,UAAU;AAC/B,6BAA2B,MAAM,CAAC,GAAG,aAAa,MAAM,EAAE,MAAM;GAChE;UACO,OAAO,YAAY,YAAY,YAAY,MACpD;OAAK,MAAM,OAAO,QAChB,KAAI,OAAO,OAAO,SAAS,IAAI,CAC7B,4BAA2B,QAAQ,MAAM,CAAC,GAAG,aAAa,IAAI,EAAE,MAAM;;AAK5E,QAAO;EACL,kBAAkB,MAAM;EACxB,wBAAwB,MAAM;EAC/B;;AAGH,MAAa,6BACX,aACA,uBACA,yBACQ;CACR,MAAM,SAAS,gBAAgB,YAAY;AAE3C,MAAK,MAAM,EAAE,OAAO,MAAM,iBAAiB,uBAAuB;EAChE,IAAI,kBAAkB,qBAAqB;AAE3C,MAAI,oBAAoB,QAAW;AACjC,OAAI,YACF,MAAK,MAAM,CAAC,aAAa,gBAAgB,OAAO,QAAQ,YAAY,CAClE,mBAAkB,gBAAgB,QAAQ,aAAa,YAAY;GAIvE,IAAI,UAAU;AACd,QAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,IACnC,WAAU,QAAQ,KAAK;AAEzB,WAAQ,KAAK,KAAK,SAAS,MAAM;;;AAIrC,QAAO"}
1
+ {"version":3,"file":"extractTranslatableContent.mjs","names":[],"sources":["../../../src/fill/extractTranslatableContent.ts"],"sourcesContent":["export type ExtractedContentProps = {\n index: number;\n path: (string | number)[];\n value: string;\n replacement?: Record<string, string>;\n};\n\nexport type ExtractedContentResult = {\n extractedContent: ExtractedContentProps[];\n translatableDictionary: Record<number, string>;\n};\n\nexport const extractTranslatableContent = (\n content: any,\n currentPath: (string | number)[] = [],\n state = {\n currentIndex: 1,\n extractedContent: [] as ExtractedContentProps[],\n translatableDictionary: {} as Record<number, string>,\n }\n): ExtractedContentResult => {\n if (typeof content === 'string') {\n const regex = /[{]+.*?[}]+/g;\n const replacement: Record<string, string> = {};\n let varIndex = 1;\n\n const modifiedContent = content.replace(regex, (matchStr) => {\n const placeholder = `<${varIndex}>`;\n replacement[placeholder] = matchStr;\n varIndex++;\n return placeholder;\n });\n\n // Only extract strings that contain at least one letter or number outside of placeholders.\n // This avoids extracting strings that are only spaces, special characters, or just variables.\n const contentWithoutPlaceholders = modifiedContent.replace(/<\\d+>/g, '');\n if (/[\\p{L}\\p{N}]/u.test(contentWithoutPlaceholders)) {\n state.extractedContent.push({\n index: state.currentIndex,\n path: currentPath,\n value: modifiedContent,\n replacement:\n Object.keys(replacement).length > 0 ? replacement : undefined,\n });\n state.translatableDictionary[state.currentIndex] = modifiedContent;\n state.currentIndex++;\n }\n } else if (Array.isArray(content)) {\n content.forEach((item, index) => {\n extractTranslatableContent(item, [...currentPath, index], state);\n });\n } else if (typeof content === 'object' && content !== null) {\n for (const key in content) {\n if (Object.hasOwn(content, key)) {\n extractTranslatableContent(content[key], [...currentPath, key], state);\n }\n }\n }\n\n return {\n extractedContent: state.extractedContent,\n translatableDictionary: state.translatableDictionary,\n };\n};\n\nexport const reinsertTranslatedContent = (\n baseContent: any,\n extractedContentProps: ExtractedContentProps[],\n translatedDictionary: Record<number, string>\n): any => {\n const result = structuredClone(baseContent);\n\n for (const { index, path, replacement } of extractedContentProps) {\n let translatedValue = translatedDictionary[index];\n\n if (translatedValue !== undefined) {\n if (replacement) {\n for (const [placeholder, originalVar] of Object.entries(replacement)) {\n translatedValue = translatedValue.replace(placeholder, originalVar);\n }\n }\n\n let current = result;\n for (let i = 0; i < path.length - 1; i++) {\n current = current[path[i]];\n }\n current[path[path.length - 1]] = translatedValue;\n }\n }\n\n return result;\n};\n"],"mappings":";AAYA,MAAa,8BACX,SACA,cAAmC,EAAE,EACrC,QAAQ;CACN,cAAc;CACd,kBAAkB,EAAE;CACpB,wBAAwB,EAAE;CAC3B,KAC0B;AAC3B,KAAI,OAAO,YAAY,UAAU;EAC/B,MAAM,QAAQ;EACd,MAAM,cAAsC,EAAE;EAC9C,IAAI,WAAW;EAEf,MAAM,kBAAkB,QAAQ,QAAQ,QAAQ,aAAa;GAC3D,MAAM,cAAc,IAAI,SAAS;AACjC,eAAY,eAAe;AAC3B;AACA,UAAO;IACP;EAIF,MAAM,6BAA6B,gBAAgB,QAAQ,UAAU,GAAG;AACxE,MAAI,gBAAgB,KAAK,2BAA2B,EAAE;AACpD,SAAM,iBAAiB,KAAK;IAC1B,OAAO,MAAM;IACb,MAAM;IACN,OAAO;IACP,aACE,OAAO,KAAK,YAAY,CAAC,SAAS,IAAI,cAAc;IACvD,CAAC;AACF,SAAM,uBAAuB,MAAM,gBAAgB;AACnD,SAAM;;YAEC,MAAM,QAAQ,QAAQ,CAC/B,SAAQ,SAAS,MAAM,UAAU;AAC/B,6BAA2B,MAAM,CAAC,GAAG,aAAa,MAAM,EAAE,MAAM;GAChE;UACO,OAAO,YAAY,YAAY,YAAY,MACpD;OAAK,MAAM,OAAO,QAChB,KAAI,OAAO,OAAO,SAAS,IAAI,CAC7B,4BAA2B,QAAQ,MAAM,CAAC,GAAG,aAAa,IAAI,EAAE,MAAM;;AAK5E,QAAO;EACL,kBAAkB,MAAM;EACxB,wBAAwB,MAAM;EAC/B;;AAGH,MAAa,6BACX,aACA,uBACA,yBACQ;CACR,MAAM,SAAS,gBAAgB,YAAY;AAE3C,MAAK,MAAM,EAAE,OAAO,MAAM,iBAAiB,uBAAuB;EAChE,IAAI,kBAAkB,qBAAqB;AAE3C,MAAI,oBAAoB,QAAW;AACjC,OAAI,YACF,MAAK,MAAM,CAAC,aAAa,gBAAgB,OAAO,QAAQ,YAAY,CAClE,mBAAkB,gBAAgB,QAAQ,aAAa,YAAY;GAIvE,IAAI,UAAU;AACd,QAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,IACnC,WAAU,QAAQ,KAAK;AAEzB,WAAQ,KAAK,KAAK,SAAS,MAAM;;;AAIrC,QAAO"}
@@ -13,7 +13,7 @@ import { getConfiguration } from "@intlayer/config/node";
13
13
  import { checkAISDKAccess } from "@intlayer/ai";
14
14
 
15
15
  //#region src/fill/fill.ts
16
- const NB_CONCURRENT_TRANSLATIONS = 1;
16
+ const NB_CONCURRENT_TRANSLATIONS = 7;
17
17
  /**
18
18
  * Fill translations based on the provided options.
19
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fill.mjs","names":[],"sources":["../../../src/fill/fill.ts"],"sourcesContent":["import { basename, join, relative } from 'node:path';\nimport { checkAISDKAccess } from '@intlayer/ai';\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 = 1;\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 } = aiResult;\n\n const { hasAIAccess, error } = await checkAISDKAccess(aiConfig!);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\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';\nimport { checkAISDKAccess } from '@intlayer/ai';\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 } = aiResult;\n\n const { hasAIAccess, error } = await checkAISDKAccess(aiConfig!);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\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,8 +1,7 @@
1
1
  import { deepMergeContent } from "./deepMergeContent.mjs";
2
2
  import { extractTranslatableContent, reinsertTranslatedContent } from "./extractTranslatableContent.mjs";
3
- import { getFilterMissingContentPerLocale } from "./getFilterMissingContentPerLocale.mjs";
4
3
  import { basename } from "node:path";
5
- import { chunkJSON, formatLocale, mergeChunks, reconstructFromSingleChunk, reduceObjectFormat, verifyIdenticObjectFormat } from "@intlayer/chokidar/utils";
4
+ import { chunkJSON, excludeObjectFormat, formatLocale, mergeChunks, reconstructFromSingleChunk, verifyIdenticObjectFormat } from "@intlayer/chokidar/utils";
6
5
  import * as ANSIColors from "@intlayer/config/colors";
7
6
  import { colon, colorize, colorizeNumber, colorizePath, getAppLogger } from "@intlayer/config/logger";
8
7
  import { getUnmergedDictionaries } from "@intlayer/unmerged-dictionaries-entry";
@@ -88,41 +87,42 @@ const translateDictionary = async (task, configuration, options) => {
88
87
  let targetLocaleDictionary;
89
88
  if (typeof baseUnmergedDictionary.locale === "string") {
90
89
  const targetLocaleFilePath = baseUnmergedDictionary.filePath?.replace(new RegExp(`/${task.sourceLocale}/`, "g"), `/${targetLocale}/`);
91
- const targetUnmergedDictionary = targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0;
92
- targetLocaleDictionary = targetUnmergedDictionary ?? {
90
+ targetLocaleDictionary = (targetLocaleFilePath ? unmergedDictionariesRecord[task.dictionaryKey]?.find((dict) => dict.filePath === targetLocaleFilePath && dict.locale === targetLocale) : void 0) ?? {
93
91
  key: baseUnmergedDictionary.key,
94
92
  content: {},
95
93
  filePath: targetLocaleFilePath,
96
94
  locale: targetLocale
97
95
  };
98
- if (mode === "complete") dictionaryToProcess = getFilterMissingContentPerLocale(dictionaryToProcess, targetUnmergedDictionary);
99
96
  } else {
100
97
  if (mode === "complete") dictionaryToProcess = getFilterMissingTranslationsDictionary(dictionaryToProcess, targetLocale);
101
98
  dictionaryToProcess = getPerLocaleDictionary(dictionaryToProcess, task.sourceLocale);
102
99
  targetLocaleDictionary = getPerLocaleDictionary(baseUnmergedDictionary, targetLocale);
103
100
  }
101
+ if (mode === "complete") dictionaryToProcess = {
102
+ ...dictionaryToProcess,
103
+ content: excludeObjectFormat(dictionaryToProcess.content, targetLocaleDictionary.content) ?? {}
104
+ };
104
105
  const localePreset = colon([
105
106
  colorize("[", ANSIColors.GREY_DARK),
106
107
  formatLocale(targetLocale),
107
108
  colorize("]", ANSIColors.GREY_DARK)
108
109
  ].join(""), { colSize: 18 });
109
110
  appLogger(`${task.dictionaryPreset}${localePreset} Preparing ${colorizePath(basename(targetLocaleDictionary.filePath))}`, { level: "info" });
110
- const { extractedContent, translatableDictionary } = extractTranslatableContent(dictionaryToProcess.content);
111
- const chunkedJsonContent = chunkJSON(translatableDictionary, CHUNK_SIZE);
111
+ const chunkedJsonContent = chunkJSON(dictionaryToProcess.content, CHUNK_SIZE);
112
112
  const nbOfChunks = chunkedJsonContent.length;
113
113
  if (nbOfChunks > 1) appLogger(`${task.dictionaryPreset}${localePreset} Split into ${colorizeNumber(nbOfChunks)} chunks for translation`, { level: "info" });
114
114
  const chunkResult = [];
115
115
  const chunkPromises = chunkedJsonContent.map(async (chunk) => {
116
116
  const chunkPreset = createChunkPreset(chunk.index, chunk.total);
117
117
  if (nbOfChunks > 1) appLogger(`${task.dictionaryPreset}${localePreset}${chunkPreset} Translating chunk`, { level: "info" });
118
- const chunkContent = reconstructFromSingleChunk(chunk);
119
- const presetOutputContent = reduceObjectFormat(translatableDictionary, chunkContent);
118
+ const reconstructedChunk = reconstructFromSingleChunk(chunk);
119
+ const { extractedContent: chunkExtractedContent, translatableDictionary: chunkTranslatableDictionary } = extractTranslatableContent(reconstructedChunk);
120
120
  const executeTranslation = async () => {
121
121
  return await retryManager(async () => {
122
122
  let translationResult;
123
123
  if (aiClient && aiConfig) translationResult = await aiClient.translateJSON({
124
- entryFileContent: chunkContent,
125
- presetOutputContent,
124
+ entryFileContent: chunkTranslatableDictionary,
125
+ presetOutputContent: chunkTranslatableDictionary,
126
126
  dictionaryDescription: dictionaryToProcess.description ?? metadata?.description ?? "",
127
127
  entryLocale: task.sourceLocale,
128
128
  outputLocale: targetLocale,
@@ -130,8 +130,8 @@ const translateDictionary = async (task, configuration, options) => {
130
130
  aiConfig
131
131
  });
132
132
  else translationResult = await intlayerAPI.ai.translateJSON({
133
- entryFileContent: chunkContent,
134
- presetOutputContent,
133
+ entryFileContent: chunkTranslatableDictionary,
134
+ presetOutputContent: chunkTranslatableDictionary,
135
135
  dictionaryDescription: dictionaryToProcess.description ?? metadata?.description ?? "",
136
136
  entryLocale: task.sourceLocale,
137
137
  outputLocale: targetLocale,
@@ -139,10 +139,10 @@ const translateDictionary = async (task, configuration, options) => {
139
139
  aiOptions
140
140
  }).then((result) => result.data);
141
141
  if (!translationResult?.fileContent) throw new Error("No content result");
142
- const { isIdentic, error } = verifyIdenticObjectFormat(translationResult.fileContent, chunkContent);
142
+ const { isIdentic, error } = verifyIdenticObjectFormat(translationResult.fileContent, chunkTranslatableDictionary);
143
143
  if (!isIdentic) throw new Error(`Translation result does not match expected format: ${error}`);
144
144
  notifySuccess();
145
- return translationResult.fileContent;
145
+ return reinsertTranslatedContent(reconstructedChunk, chunkExtractedContent, translationResult.fileContent);
146
146
  }, {
147
147
  maxRetry: MAX_RETRY,
148
148
  delay: RETRY_DELAY,
@@ -165,14 +165,12 @@ const translateDictionary = async (task, configuration, options) => {
165
165
  (await Promise.all(chunkPromises)).sort((chunkA, chunkB) => chunkA.chunk.index - chunkB.chunk.index).forEach(({ result }) => {
166
166
  chunkResult.push(result);
167
167
  });
168
- const mergedTranslatedDictionary = mergeChunks(chunkResult);
169
- const reinsertedContent = reinsertTranslatedContent(dictionaryToProcess.content, extractedContent, mergedTranslatedDictionary);
170
- let finalContent = {
168
+ const reinsertedContent = mergeChunks(chunkResult);
169
+ const merged = {
171
170
  ...dictionaryToProcess,
172
171
  content: reinsertedContent
173
- }.content;
174
- if (typeof baseUnmergedDictionary.locale === "string") finalContent = deepMergeContent(targetLocaleDictionary.content ?? {}, finalContent);
175
- return [targetLocale, finalContent];
172
+ };
173
+ return [targetLocale, deepMergeContent(targetLocaleDictionary.content ?? {}, merged.content)];
176
174
  }));
177
175
  const translatedContent = Object.fromEntries(translatedContentResults);
178
176
  let dictionaryOutput = {
@@ -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 formatLocale,\n type JsonChunk,\n mergeChunks,\n reconstructFromSingleChunk,\n reduceObjectFormat,\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 { getFilterMissingContentPerLocale } from './getFilterMissingContentPerLocale';\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\n // In complete mode, filter out already translated content\n if (mode === 'complete') {\n dictionaryToProcess = getFilterMissingContentPerLocale(\n dictionaryToProcess,\n targetUnmergedDictionary\n );\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 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 { extractedContent, translatableDictionary } =\n extractTranslatableContent(dictionaryToProcess.content);\n\n const chunkedJsonContent: JsonChunk[] = chunkJSON(\n translatableDictionary 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 chunkContent = reconstructFromSingleChunk(chunk);\n const presetOutputContent = reduceObjectFormat(\n translatableDictionary,\n chunkContent\n ) as unknown as JSON;\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: chunkContent as unknown as JSON,\n presetOutputContent,\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: chunkContent as unknown as JSON,\n presetOutputContent,\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 chunkContent\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 translationResult.fileContent;\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 partial JSON objects produced from each chunk into a single object\n const mergedTranslatedDictionary = mergeChunks(chunkResult);\n\n const reinsertedContent = reinsertTranslatedContent(\n dictionaryToProcess.content,\n extractedContent,\n mergedTranslatedDictionary as Record<number, string>\n );\n\n const merged = {\n ...dictionaryToProcess,\n content: reinsertedContent,\n };\n\n // For per-locale files, merge the newly translated content with existing target content\n let finalContent = merged.content;\n\n if (typeof baseUnmergedDictionary.locale === 'string') {\n // Deep merge: existing content + newly translated content\n finalContent = deepMergeContent(\n targetLocaleDictionary.content ?? {},\n finalContent\n );\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 = 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 +1 @@
1
- {"version":3,"file":"translateDictionary.d.ts","names":[],"sources":["../../../src/fill/translateDictionary.ts"],"mappings":";;;;;;;;;KAwCK,yBAAA,GAA4B,eAAA;EAC/B,gBAAA,EAAkB,UAAA;AAAA;AAAA,KAGf,0BAAA;EACH,IAAA;EACA,SAAA,GAAY,SAAA;EACZ,YAAA;EACA,QAAA,GAAW,UAAA,QAFU,2BAAA,CAGuB,gBAAA;EAE5C,SAAA;EACA,OAAA,IAAW,KAAA;EACX,aAAA,SAAsB,KAAA;EACtB,QAAA,GAAW,QAAA;EACX,QAAA,GAAW,QAAA;AAAA;AAAA,cAyCA,mBAAA,GACX,IAAA,EAAM,eAAA,EACN,aAAA,EAAe,cAAA,EACf,OAAA,GAAU,0BAAA,KACT,OAAA,CAAQ,yBAAA"}
1
+ {"version":3,"file":"translateDictionary.d.ts","names":[],"sources":["../../../src/fill/translateDictionary.ts"],"mappings":";;;;;;;;;KAuCK,yBAAA,GAA4B,eAAA;EAC/B,gBAAA,EAAkB,UAAA;AAAA;AAAA,KAGf,0BAAA;EACH,IAAA;EACA,SAAA,GAAY,SAAA;EACZ,YAAA;EACA,QAAA,GAAW,UAAA,QAFU,2BAAA,CAGuB,gBAAA;EAE5C,SAAA;EACA,OAAA,IAAW,KAAA;EACX,aAAA,SAAsB,KAAA;EACtB,QAAA,GAAW,QAAA;EACX,QAAA,GAAW,QAAA;AAAA;AAAA,cAyCA,mBAAA,GACX,IAAA,EAAM,eAAA,EACN,aAAA,EAAe,cAAA,EACf,OAAA,GAAU,0BAAA,KACT,OAAA,CAAQ,yBAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/cli",
3
- "version": "8.6.2",
3
+ "version": "8.6.4",
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,16 +67,16 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@clack/prompts": "0.11.0",
70
- "@intlayer/ai": "8.6.2",
71
- "@intlayer/api": "8.6.2",
72
- "@intlayer/babel": "8.6.2",
73
- "@intlayer/chokidar": "8.6.2",
74
- "@intlayer/config": "8.6.2",
75
- "@intlayer/core": "8.6.2",
76
- "@intlayer/dictionaries-entry": "8.6.2",
77
- "@intlayer/remote-dictionaries-entry": "8.6.2",
78
- "@intlayer/types": "8.6.2",
79
- "@intlayer/unmerged-dictionaries-entry": "8.6.2",
70
+ "@intlayer/ai": "8.6.4",
71
+ "@intlayer/api": "8.6.4",
72
+ "@intlayer/babel": "8.6.4",
73
+ "@intlayer/chokidar": "8.6.4",
74
+ "@intlayer/config": "8.6.4",
75
+ "@intlayer/core": "8.6.4",
76
+ "@intlayer/dictionaries-entry": "8.6.4",
77
+ "@intlayer/remote-dictionaries-entry": "8.6.4",
78
+ "@intlayer/types": "8.6.4",
79
+ "@intlayer/unmerged-dictionaries-entry": "8.6.4",
80
80
  "commander": "14.0.3",
81
81
  "enquirer": "^2.4.1",
82
82
  "eventsource": "4.1.0",
@@ -93,7 +93,7 @@
93
93
  "vitest": "4.1.2"
94
94
  },
95
95
  "peerDependencies": {
96
- "@intlayer/ai": "8.6.2"
96
+ "@intlayer/ai": "8.6.4"
97
97
  },
98
98
  "peerDependenciesMeta": {
99
99
  "@intlayer/ai": {