{"version":3,"file":"index.mjs","names":["LokaliseApiError"],"sources":["../lib/errors/LokaliseError.ts","../lib/services/LokaliseFileExchange.ts","../lib/services/LokaliseDownload.ts","../lib/services/LokaliseUpload.ts"],"sourcesContent":["import type { LokaliseError as ILokaliseError } from \"../interfaces/LokaliseError.js\";\n\n/**\n * Represents a custom error.\n */\nexport class LokaliseError extends Error implements ILokaliseError {\n\t/**\n\t * The error code representing the type of Lokalise API error.\n\t */\n\tcode?: number | undefined;\n\n\t/**\n\t * Additional details about the error.\n\t */\n\tdetails?: Record<string, string | number | boolean>;\n\n\t/**\n\t * Creates a new instance of LokaliseError.\n\t *\n\t * @param message - The error message.\n\t * @param code - The error code (optional).\n\t * @param details - Optional additional details about the error.\n\t */\n\tconstructor(\n\t\tmessage: string,\n\t\tcode?: number,\n\t\tdetails?: Record<string, string | number | boolean>,\n\t) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\n\t\tif (details) {\n\t\t\tthis.details = details;\n\t\t}\n\t}\n\n\t/**\n\t * Returns a string representation of the error, including code and details.\n\t *\n\t * @returns The formatted error message.\n\t */\n\toverride toString(): string {\n\t\tlet baseMessage = `LokaliseError: ${this.message}`;\n\t\tif (this.code) {\n\t\t\tbaseMessage += ` (Code: ${this.code})`;\n\t\t}\n\t\tif (this.details) {\n\t\t\tconst formattedDetails = Object.entries(this.details)\n\t\t\t\t.map(([key, value]) => `${key}: ${value}`)\n\t\t\t\t.join(\", \");\n\n\t\t\tbaseMessage += ` | Details: ${formattedDetails}`;\n\t\t}\n\t\treturn baseMessage;\n\t}\n}\n","import type { ClientParams, QueuedProcess } from \"@lokalise/node-api\";\nimport {\n\tLokaliseApi,\n\tApiError as LokaliseApiError,\n\tLokaliseApiOAuth,\n} from \"@lokalise/node-api\";\nimport {\n\ttype LogFunction,\n\ttype LogLevel,\n\ttype LogThreshold,\n\tlogWithColor,\n\tlogWithLevel,\n} from \"kliedz\";\nimport { LokaliseError } from \"../errors/LokaliseError.js\";\nimport type {\n\tLokaliseExchangeConfig,\n\tRetryParams,\n} from \"../interfaces/index.js\";\n\n/**\n * A utility class for exchanging files with the Lokalise API.\n */\nexport class LokaliseFileExchange {\n\t/**\n\t * The Lokalise API client instance.\n\t */\n\tprotected readonly apiClient: LokaliseApi;\n\n\t/**\n\t * The ID of the project in Lokalise.\n\t */\n\tprotected readonly projectId: string;\n\n\t/**\n\t * Retry parameters for API requests.\n\t */\n\tprotected readonly retryParams: RetryParams;\n\n\t/**\n\t * Logger function.\n\t */\n\tprotected readonly logger: LogFunction;\n\n\t/**\n\t * Log threshold (do not print messages with severity less than the specified value).\n\t */\n\tprotected readonly logThreshold: LogThreshold;\n\n\t/**\n\t * Default retry parameters for API requests.\n\t */\n\tprivate static readonly defaultRetryParams: Required<RetryParams> = {\n\t\tmaxRetries: 3,\n\t\tinitialSleepTime: 1000,\n\t\tjitterRatio: 0.2,\n\t\trng: Math.random,\n\t};\n\n\tprivate static readonly FINISHED_STATUSES = [\n\t\t\"finished\",\n\t\t\"cancelled\",\n\t\t\"failed\",\n\t] as const;\n\n\tprivate static readonly RETRYABLE_CODES = [408, 429];\n\n\tprotected static readonly maxConcurrentProcesses = 6;\n\n\tprivate static isPendingStatus(status?: string | null): boolean {\n\t\treturn !LokaliseFileExchange.isFinishedStatus(status);\n\t}\n\n\tpublic static isFinishedStatus(status?: string | null): boolean {\n\t\treturn (\n\t\t\tstatus != null &&\n\t\t\t(LokaliseFileExchange.FINISHED_STATUSES as readonly string[]).includes(\n\t\t\t\tstatus,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Creates a new instance of LokaliseFileExchange.\n\t *\n\t * @param clientConfig - Configuration for the Lokalise SDK.\n\t * @param exchangeConfig - The configuration object for file exchange operations.\n\t * @throws {LokaliseError} If the provided configuration is invalid.\n\t */\n\tconstructor(\n\t\tclientConfig: ClientParams,\n\t\t{\n\t\t\tprojectId,\n\t\t\tuseOAuth2 = false,\n\t\t\tretryParams,\n\t\t\tlogThreshold = \"info\",\n\t\t\tlogColor = true,\n\t\t}: LokaliseExchangeConfig,\n\t) {\n\t\tthis.projectId = projectId;\n\t\tthis.logThreshold = logThreshold;\n\t\tthis.logger = this.chooseLogger(logColor);\n\t\tthis.retryParams = this.buildRetryParams(retryParams);\n\n\t\tthis.validateParams();\n\n\t\tconst apiConfig = this.buildLokaliseClientConfig(\n\t\t\tclientConfig,\n\t\t\tlogThreshold,\n\t\t);\n\t\tthis.apiClient = this.createApiClient(apiConfig, useOAuth2);\n\t}\n\n\t/**\n\t * Executes an asynchronous operation with exponential-backoff retry logic.\n\t *\n\t * The operation is attempted multiple times if it throws a retryable\n\t * `LokaliseApiError`. Each retry waits longer than the previous one based on\n\t * exponential backoff parameters (`base`, `factor`, optional jitter).\n\t *\n\t * Behaviour:\n\t * - If the operation succeeds — its result is returned immediately.\n\t * - If it fails with a retryable error — the function waits and retries.\n\t * - If the maximum number of retries is reached — throws a `LokaliseError`\n\t *   with the original error details.\n\t * - If the error is non-retryable — it is immediately wrapped into\n\t *   `LokaliseError` and rethrown.\n\t * - Any non-Lokalise errors are rethrown as-is.\n\t *\n\t * @param operation - A function that performs the async action to be retried.\n\t * @returns The successful result of the operation.\n\t * @throws LokaliseError After all retries fail or on non-retryable errors.\n\t */\n\tprotected async withExponentialBackoff<T>(\n\t\toperation: () => Promise<T>,\n\t): Promise<T> {\n\t\tconst { maxRetries } = this.retryParams;\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Running operation with exponential backoff; max retries: ${maxRetries}`,\n\t\t);\n\n\t\tfor (let attempt = 1; attempt <= maxRetries + 1; attempt++) {\n\t\t\ttry {\n\t\t\t\tthis.logMsg(\"debug\", `Attempt #${attempt}...`);\n\t\t\t\treturn await operation();\n\t\t\t} catch (error: unknown) {\n\t\t\t\tif (error instanceof LokaliseApiError && this.isRetryable(error)) {\n\t\t\t\t\tthis.logMsg(\"debug\", `Retryable error caught: ${error.message}`);\n\n\t\t\t\t\tif (attempt === maxRetries + 1) {\n\t\t\t\t\t\tthrow new LokaliseError(\n\t\t\t\t\t\t\t`Maximum retries reached: ${error.message}`,\n\t\t\t\t\t\t\terror.code,\n\t\t\t\t\t\t\terror.details,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tconst sleepMs = this.calculateSleepMs(this.retryParams, attempt);\n\n\t\t\t\t\tthis.logMsg(\"debug\", `Waiting ${sleepMs}ms before retry...`);\n\t\t\t\t\tawait LokaliseFileExchange.sleep(sleepMs);\n\t\t\t\t} else if (error instanceof LokaliseApiError) {\n\t\t\t\t\tthrow new LokaliseError(error.message, error.code, error.details);\n\t\t\t\t} else {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// This line is unreachable but keeps TS happy.\n\t\tthrow new LokaliseError(\"Unexpected error during operation.\", 500);\n\t}\n\t/**\n\t * Polls the status of queued processes until they are marked as \"finished\"\n\t * or until the maximum wait time is exceeded.\n\t *\n\t * Uses batched polling with limited concurrency and exponential backoff-like\n\t * wait times between iterations. Performs an initial status snapshot, then\n\t * repeatedly refreshes pending processes until either:\n\t * - all of them reach a finished state, or\n\t * - the time budget (`maxWaitTime`) is exhausted.\n\t *\n\t * A final refresh is performed for any still-pending processes before returning.\n\t *\n\t * @param processes - List of queued processes to poll.\n\t * @param initialWaitTime - Initial delay (in ms) between polling iterations.\n\t * @param maxWaitTime - Maximum total time (in ms) allowed for polling.\n\t * @param concurrency - Maximum number of processes to refresh per batch.\n\t * @returns A list of processes with their latest known statuses.\n\t */\n\tprotected async pollProcesses(\n\t\tprocesses: QueuedProcess[],\n\t\tinitialWaitTime: number,\n\t\tmaxWaitTime: number,\n\t\tconcurrency = LokaliseFileExchange.maxConcurrentProcesses,\n\t): Promise<QueuedProcess[]> {\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Start polling processes. Total processes count: ${processes.length}`,\n\t\t);\n\n\t\tconst startTime = Date.now();\n\n\t\tconst { processMap, pendingProcessIds } =\n\t\t\tthis.initializePollingState(processes);\n\n\t\tawait this.runPollingLoop(\n\t\t\tprocessMap,\n\t\t\tpendingProcessIds,\n\t\t\tstartTime,\n\t\t\tinitialWaitTime,\n\t\t\tmaxWaitTime,\n\t\t\tconcurrency,\n\t\t);\n\n\t\tif (pendingProcessIds.size > 0) {\n\t\t\tawait this.refreshRemainingProcesses(\n\t\t\t\tprocessMap,\n\t\t\t\tpendingProcessIds,\n\t\t\t\tconcurrency,\n\t\t\t);\n\t\t}\n\n\t\treturn Array.from(processMap.values());\n\t}\n\n\t/**\n\t * Builds internal tracking structures for polling: a map of process IDs\n\t * to their last known state and a set of IDs that are still pending.\n\t *\n\t * Also logs the initial status of each process.\n\t *\n\t * @param processes - Initial list of queued processes.\n\t * @returns A map of processes keyed by ID and a set of pending process IDs.\n\t */\n\tprivate initializePollingState(processes: QueuedProcess[]): {\n\t\tprocessMap: Map<string, QueuedProcess>;\n\t\tpendingProcessIds: Set<string>;\n\t} {\n\t\tthis.logMsg(\"debug\", \"Initial processes check...\");\n\n\t\tconst processMap = new Map<string, QueuedProcess>();\n\t\tconst pendingProcessIds = new Set<string>();\n\n\t\tfor (const p of processes) {\n\t\t\tif (p.status) {\n\t\t\t\tthis.logMsg(\n\t\t\t\t\t\"debug\",\n\t\t\t\t\t`Process ID: ${p.process_id}, status: ${p.status}`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.logMsg(\"debug\", `Process ID: ${p.process_id}, status is missing`);\n\t\t\t}\n\n\t\t\tprocessMap.set(p.process_id, p);\n\n\t\t\tif (LokaliseFileExchange.isPendingStatus(p.status)) {\n\t\t\t\tpendingProcessIds.add(p.process_id);\n\t\t\t}\n\t\t}\n\n\t\treturn { processMap, pendingProcessIds };\n\t}\n\n\t/**\n\t * Runs the main polling loop for the given processes.\n\t *\n\t * Repeatedly fetches updated process statuses in batches while:\n\t * - there are still pending IDs, and\n\t * - the elapsed time is below the configured maximum.\n\t *\n\t * Includes a small \"fast-follow\" recheck when some processes have missing\n\t * status on the first iterations, and uses a growing wait time between\n\t * iterations (capped by the remaining time budget).\n\t *\n\t * @param processMap - Map of process IDs to their last known state.\n\t * @param pendingProcessIds - Set of IDs that are not finished yet.\n\t * @param startTime - Timestamp (ms) when polling started.\n\t * @param initialWaitTime - Initial delay (ms) between polling iterations.\n\t * @param maxWaitTime - Maximum total polling duration (ms).\n\t * @param concurrency - Maximum number of processes to refresh per batch.\n\t */\n\tprivate async runPollingLoop(\n\t\tprocessMap: Map<string, QueuedProcess>,\n\t\tpendingProcessIds: Set<string>,\n\t\tstartTime: number,\n\t\tinitialWaitTime: number,\n\t\tmaxWaitTime: number,\n\t\tconcurrency: number,\n\t): Promise<void> {\n\t\tlet waitTime = initialWaitTime;\n\t\tlet didFastFollow = false;\n\n\t\twhile (pendingProcessIds.size > 0 && Date.now() - startTime < maxWaitTime) {\n\t\t\tthis.logMsg(\"debug\", `Polling... Pending IDs: ${pendingProcessIds.size}`);\n\n\t\t\tif (\n\t\t\t\t!didFastFollow &&\n\t\t\t\t[...processMap.values()].some((p) => p.status == null)\n\t\t\t) {\n\t\t\t\tthis.logMsg(\n\t\t\t\t\t\"debug\",\n\t\t\t\t\t\"Fast-follow: some statuses missing, quick recheck in 200ms\",\n\t\t\t\t);\n\t\t\t\tawait LokaliseFileExchange.sleep(200);\n\t\t\t\tdidFastFollow = true;\n\t\t\t}\n\n\t\t\tconst ids = [...pendingProcessIds];\n\t\t\tconst batch = await this.fetchProcessesBatch(ids, concurrency);\n\n\t\t\tfor (const { id, process } of batch) {\n\t\t\t\tif (!process) continue;\n\t\t\t\tprocessMap.set(id, process);\n\n\t\t\t\tif (LokaliseFileExchange.isFinishedStatus(process.status)) {\n\t\t\t\t\tthis.logMsg(\n\t\t\t\t\t\t\"debug\",\n\t\t\t\t\t\t`Process ${id} completed with status=${process.status}.`,\n\t\t\t\t\t);\n\t\t\t\t\tpendingProcessIds.delete(id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (pendingProcessIds.size === 0) {\n\t\t\t\tthis.logMsg(\"debug\", \"Finished polling. Pending processes IDs: 0\");\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst elapsed = Date.now() - startTime;\n\t\t\tconst remaining = maxWaitTime - elapsed;\n\n\t\t\tif (remaining <= 0) {\n\t\t\t\tthis.logMsg(\n\t\t\t\t\t\"debug\",\n\t\t\t\t\t\"Time budget exhausted, stopping polling without extra sleep.\",\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst sleepMs = Math.min(waitTime, remaining);\n\t\t\tthis.logMsg(\"debug\", `Waiting ${sleepMs}...`);\n\t\t\tawait LokaliseFileExchange.sleep(sleepMs);\n\n\t\t\twaitTime = Math.min(\n\t\t\t\twaitTime * 2,\n\t\t\t\tMath.max(0, maxWaitTime - (Date.now() - startTime)),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Performs a final status refresh for any processes that are still marked\n\t * as pending after the main polling loop.\n\t *\n\t * This gives one last chance to capture terminal statuses right before\n\t * returning the result to the caller.\n\t *\n\t * @param processMap - Map of process IDs to their last known state.\n\t * @param pendingProcessIds - Set of IDs that are still considered pending.\n\t * @param concurrency - Maximum number of processes to refresh per batch.\n\t */\n\tprivate async refreshRemainingProcesses(\n\t\tprocessMap: Map<string, QueuedProcess>,\n\t\tpendingProcessIds: Set<string>,\n\t\tconcurrency: number,\n\t): Promise<void> {\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Final refresh for ${pendingProcessIds.size} pending processes before return...`,\n\t\t);\n\n\t\tconst finalBatch = await this.fetchProcessesBatch(\n\t\t\t[...pendingProcessIds],\n\t\t\tconcurrency,\n\t\t);\n\n\t\tfor (const { id, process } of finalBatch) {\n\t\t\tif (process) {\n\t\t\t\tprocessMap.set(id, process);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Determines whether the given Lokalise API error should trigger a retry attempt.\n\t *\n\t * An error is considered retryable if its `code` matches one of the predefined\n\t * retryable status codes.\n\t *\n\t * @param error - The `LokaliseApiError` instance to evaluate.\n\t * @returns `true` if the error is retryable, otherwise `false`.\n\t */\n\tprivate isRetryable(error: LokaliseApiError): boolean {\n\t\treturn LokaliseFileExchange.RETRYABLE_CODES.includes(error.code);\n\t}\n\n\t/**\n\t * Logs a message using the configured logger, respecting the current log threshold.\n\t *\n\t * Wraps the raw logger call by attaching metadata such as:\n\t * - `level` — severity of the log entry,\n\t * - `threshold` — active log level threshold used to filter messages,\n\t * - `withTimestamp` — instructs the logger to prepend a timestamp.\n\t *\n\t * All variadic `args` are forwarded directly to the logger.\n\t *\n\t * @param level - Log level of the message being emitted.\n\t * @param args - Additional values to pass to the logger.\n\t */\n\tprotected logMsg(level: LogLevel, ...args: unknown[]): void {\n\t\tthis.logger(\n\t\t\t{ level, threshold: this.logThreshold, withTimestamp: true },\n\t\t\t...args,\n\t\t);\n\t}\n\n\t/**\n\t * Fetches the most recent state of a queued process from the Lokalise API.\n\t *\n\t * Sends a GET request for the process identified by `processId` and logs\n\t * both the request and the received status. Used during polling to refresh\n\t * the status of long-running async operations.\n\t *\n\t * @param processId - The unique identifier of the queued process to retrieve.\n\t * @returns A promise resolving to the updated `QueuedProcess` object.\n\t */\n\tprotected async getUpdatedProcess(processId: string): Promise<QueuedProcess> {\n\t\tthis.logMsg(\"debug\", `Requesting update for process ID: ${processId}`);\n\n\t\tconst updatedProcess = await this.apiClient\n\t\t\t.queuedProcesses()\n\t\t\t.get(processId, { project_id: this.projectId });\n\n\t\tif (updatedProcess.status) {\n\t\t\tthis.logMsg(\n\t\t\t\t\"debug\",\n\t\t\t\t`Process ID: ${updatedProcess.process_id}, status: ${updatedProcess.status}`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.logMsg(\n\t\t\t\t\"debug\",\n\t\t\t\t`Process ID: ${updatedProcess.process_id}, status is missing`,\n\t\t\t);\n\t\t}\n\n\t\treturn updatedProcess;\n\t}\n\n\t/**\n\t * Validates essential client configuration parameters before any operations run.\n\t *\n\t * Ensures that:\n\t * - `projectId` is present and is a non-empty string,\n\t * - retry settings (`maxRetries`, `initialSleepTime`, `jitterRatio`)\n\t *   fall within acceptable ranges.\n\t *\n\t * Throws a `LokaliseError` if any configuration parameter is missing,\n\t * malformed, or outside allowed bounds.\n\t */\n\tprivate validateParams(): void {\n\t\tif (!this.projectId || typeof this.projectId !== \"string\") {\n\t\t\tthrow new LokaliseError(\"Invalid or missing Project ID.\");\n\t\t}\n\n\t\tconst { maxRetries, initialSleepTime, jitterRatio } = this.retryParams;\n\n\t\tif (maxRetries < 0) {\n\t\t\tthrow new LokaliseError(\n\t\t\t\t\"maxRetries must be greater than or equal to zero.\",\n\t\t\t);\n\t\t}\n\t\tif (initialSleepTime <= 0) {\n\t\t\tthrow new LokaliseError(\"initialSleepTime must be a positive value.\");\n\t\t}\n\t\tif (jitterRatio < 0 || jitterRatio > 1)\n\t\t\tthrow new LokaliseError(\"jitterRatio must be between 0 and 1.\");\n\t}\n\n\t/**\n\t * Executes asynchronous work over a list of items with a fixed concurrency limit.\n\t *\n\t * Spawns up to `limit` parallel worker loops. Each loop pulls the next\n\t * unprocessed item index in a thread-safe manner (via shared counter `i`),\n\t * runs the provided async `worker` function for that item, and stores the\n\t * resulting value in the corresponding position of the `results` array.\n\t *\n\t * Processing stops when all items have been consumed. If any worker throws,\n\t * the error propagates and the whole operation rejects.\n\t *\n\t * @param items - The list of items to process.\n\t * @param limit - Maximum number of concurrent async operations.\n\t * @param worker - Async handler executed for each item.\n\t * @returns A promise resolving to an array of results, preserving input order.\n\t */\n\tprotected async runWithConcurrencyLimit<T, R>(\n\t\titems: T[],\n\t\tlimit: number,\n\t\tworker: (item: T, index: number) => Promise<R>,\n\t): Promise<R[]> {\n\t\tconst results = new Array<R>(items.length);\n\t\tlet i = 0;\n\n\t\tconst workers = new Array(Math.min(limit, items.length))\n\t\t\t.fill(null)\n\t\t\t.map(async () => {\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst idx = i++;\n\t\t\t\t\tif (idx >= items.length) break;\n\t\t\t\t\tconst item = items[idx];\n\t\t\t\t\tif (item === undefined) {\n\t\t\t\t\t\tthrow new Error(`Missing item at index ${idx}`);\n\t\t\t\t\t}\n\n\t\t\t\t\tresults[idx] = await worker(item, idx);\n\t\t\t\t}\n\t\t\t});\n\n\t\tawait Promise.all(workers);\n\t\treturn results;\n\t}\n\n\t/**\n\t * Fetches updated process states for a list of process IDs in parallel,\n\t * respecting a maximum concurrency limit.\n\t *\n\t * Each process ID is resolved via `getUpdatedProcess()`. If the fetch\n\t * succeeds, the returned object includes both `id` and the updated\n\t * `process`. If an error occurs, a warning is logged and the result\n\t * contains only the `id`, allowing polling to continue without failing\n\t * the entire batch.\n\t *\n\t * Internally uses `runWithConcurrencyLimit` to enforce controlled parallelism.\n\t *\n\t * @param processIds - The list of queued process IDs to refresh.\n\t * @param concurrency - Maximum number of simultaneous requests.\n\t * @returns A list of objects mapping each ID to its latest fetched state\n\t *          (or `undefined` if the fetch failed).\n\t */\n\tprotected async fetchProcessesBatch(\n\t\tprocessIds: string[],\n\t\tconcurrency = LokaliseFileExchange.maxConcurrentProcesses,\n\t): Promise<Array<{ id: string; process?: QueuedProcess }>> {\n\t\treturn this.runWithConcurrencyLimit(processIds, concurrency, async (id) => {\n\t\t\ttry {\n\t\t\t\tconst updated = await this.getUpdatedProcess(id);\n\t\t\t\treturn { id, process: updated };\n\t\t\t} catch (error) {\n\t\t\t\tthis.logMsg(\"warn\", `Failed to fetch process ${id}:`, error);\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Delays execution for a given duration.\n\t *\n\t * Creates a Promise that resolves after the specified number of milliseconds,\n\t * allowing async workflows to pause without blocking the event loop.\n\t *\n\t * @param ms - Number of milliseconds to wait.\n\t * @returns A promise that resolves after the delay.\n\t */\n\tprotected static sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n\n\t/**\n\t * Computes the exponential-backoff delay for a retry attempt,\n\t * optionally adding jitter to avoid synchronized retries.\n\t *\n\t * @param retryParams - Backoff settings (initial delay, jitter, RNG).\n\t * @param attempt - Retry attempt number (1-based).\n\t * @returns Calculated sleep time in milliseconds.\n\t */\n\tprotected calculateSleepMs(\n\t\tretryParams: RetryParams,\n\t\tattempt: number,\n\t): number {\n\t\tconst { initialSleepTime, jitterRatio, rng } = retryParams;\n\n\t\tconst base = initialSleepTime * 2 ** (attempt - 1);\n\t\tconst maxJitter = Math.floor(base * jitterRatio);\n\t\tconst jitter = maxJitter > 0 ? Math.floor(rng() * maxJitter) : 0;\n\t\treturn base + jitter;\n\t}\n\n\t/**\n\t * Builds the final Lokalise client configuration,\n\t * enabling silent mode when the log threshold is `\"silent\"`.\n\t *\n\t * @param clientConfig - Base client parameters.\n\t * @param logThreshold - Active logging threshold.\n\t * @returns The adjusted client configuration.\n\t */\n\tprivate buildLokaliseClientConfig(\n\t\tclientConfig: ClientParams,\n\t\tlogThreshold: LogThreshold,\n\t): ClientParams {\n\t\tif (logThreshold === \"silent\") {\n\t\t\treturn {\n\t\t\t\t...clientConfig,\n\t\t\t\tsilent: true,\n\t\t\t};\n\t\t}\n\n\t\treturn { ...clientConfig };\n\t}\n\n\t/**\n\t * Creates the appropriate Lokalise API client instance,\n\t * choosing between OAuth2 and token-based authentication.\n\t *\n\t * @param lokaliseApiConfig - Configuration passed to the client.\n\t * @param useOAuth2 - Whether OAuth2 authentication should be used.\n\t * @returns A Lokalise API client instance.\n\t */\n\tprivate createApiClient(\n\t\tlokaliseApiConfig: ClientParams,\n\t\tuseOAuth2: boolean,\n\t): LokaliseApi | LokaliseApiOAuth {\n\t\tif (useOAuth2) {\n\t\t\tthis.logMsg(\"debug\", \"Using OAuth 2 Lokalise API client\");\n\t\t\treturn new LokaliseApiOAuth(lokaliseApiConfig);\n\t\t}\n\n\t\tthis.logMsg(\"debug\", \"Using regular (token-based) Lokalise API client\");\n\t\treturn new LokaliseApi(lokaliseApiConfig);\n\t}\n\n\t/**\n\t * Merges user-provided retry settings with default retry parameters.\n\t *\n\t * @param retryParams - Optional overrides.\n\t * @returns Fully resolved retry configuration.\n\t */\n\tprivate buildRetryParams(retryParams?: Partial<RetryParams>): RetryParams {\n\t\treturn {\n\t\t\t...LokaliseFileExchange.defaultRetryParams,\n\t\t\t...retryParams,\n\t\t};\n\t}\n\n\t/**\n\t * Selects the logger implementation based on whether color output is enabled.\n\t *\n\t * @param logColor - If true, uses the colorized logger.\n\t * @returns The chosen log function.\n\t */\n\tprivate chooseLogger(logColor: boolean): LogFunction {\n\t\treturn logColor ? logWithColor : logWithLevel;\n\t}\n}\n","import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { pipeline, Readable } from \"node:stream\";\nimport type { ReadableStream as WebReadableStream } from \"node:stream/web\";\nimport { promisify } from \"node:util\";\nimport type {\n\tDownloadBundle,\n\tDownloadedFileProcessDetails,\n\tDownloadFileParams,\n\tQueuedProcess,\n} from \"@lokalise/node-api\";\nimport yauzl from \"yauzl\";\nimport { LokaliseError } from \"../errors/LokaliseError.js\";\nimport type {\n\tDownloadTranslationParams,\n\tProcessDownloadFileParams,\n} from \"../interfaces/index.js\";\nimport { LokaliseFileExchange } from \"./LokaliseFileExchange.js\";\n\n/**\n * Handles downloading and extracting translation files from Lokalise.\n */\nexport class LokaliseDownload extends LokaliseFileExchange {\n\tprivate static readonly defaultProcessParams: Required<ProcessDownloadFileParams> =\n\t\t{\n\t\t\tasyncDownload: false,\n\t\t\tpollInitialWaitTime: 1000,\n\t\t\tpollMaximumWaitTime: 120_000,\n\t\t\tbundleDownloadTimeout: 0,\n\t\t};\n\n\tprivate readonly streamPipeline = promisify(pipeline);\n\n\t/**\n\t * Downloads translations from Lokalise, optionally using async polling, and extracts them to disk.\n\t *\n\t * @param downloadTranslationParams - Full configuration for the download process, extraction destination, and optional polling or timeout settings.\n\t * @throws {LokaliseError} If the download, polling, or extraction fails.\n\t */\n\tasync downloadTranslations({\n\t\tdownloadFileParams,\n\t\textractParams = {},\n\t\tprocessDownloadFileParams,\n\t}: DownloadTranslationParams): Promise<void> {\n\t\tthis.logMsg(\"debug\", \"Downloading translations from Lokalise...\");\n\n\t\tconst processParams = this.buildProcessParams(processDownloadFileParams);\n\n\t\tconst translationsBundleURL = await this.fetchTranslationBundleURL(\n\t\t\tdownloadFileParams,\n\t\t\tprocessParams,\n\t\t);\n\n\t\tconst zipFilePath = await this.downloadZip(\n\t\t\ttranslationsBundleURL,\n\t\t\tprocessParams.bundleDownloadTimeout,\n\t\t);\n\n\t\tawait this.processZip(\n\t\t\tzipFilePath,\n\t\t\tpath.resolve(extractParams.outputDir ?? \"./\"),\n\t\t);\n\t}\n\n\t/**\n\t * Unpacks a ZIP file into the specified directory.\n\t *\n\t * @param zipFilePath - Path to the ZIP file.\n\t * @param outputDir - Directory to extract the files into.\n\t * @throws {LokaliseError} If extraction fails or malicious paths are detected.\n\t */\n\tprotected async unpackZip(\n\t\tzipFilePath: string,\n\t\toutputDir: string,\n\t): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tyauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => {\n\t\t\t\tif (err) {\n\t\t\t\t\treturn reject(\n\t\t\t\t\t\tnew LokaliseError(\n\t\t\t\t\t\t\t`Failed to open ZIP file at ${zipFilePath}: ${err.message}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tzipfile.readEntry();\n\n\t\t\t\tzipfile.on(\"entry\", (entry) => {\n\t\t\t\t\tthis.handleZipEntry(entry, zipfile, outputDir)\n\t\t\t\t\t\t.then(() => zipfile.readEntry())\n\t\t\t\t\t\t.catch(reject);\n\t\t\t\t});\n\n\t\t\t\tzipfile.on(\"end\", resolve);\n\t\t\t\tzipfile.on(\"error\", reject);\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Downloads a ZIP file from the given URL and stores it as a temporary file.\n\t *\n\t * Performs URL validation, optional timeout handling, fetch request execution,\n\t * response integrity checks, and writes the ZIP stream to disk.\n\t *\n\t * @param url - Direct URL to the ZIP bundle provided by Lokalise.\n\t * @param downloadTimeout - Optional timeout (in ms) for the HTTP request. `0` disables timeouts.\n\t * @returns Absolute path to the temporary ZIP file on disk.\n\t */\n\tprotected async downloadZip(\n\t\turl: string,\n\t\tdownloadTimeout = 0,\n\t): Promise<string> {\n\t\tthis.logMsg(\"debug\", \"Downloading translation bundle...\");\n\n\t\tconst bundleURL = this.assertHttpUrl(url);\n\t\tconst tempZipPath = this.buildTempZipPath();\n\n\t\tconst signal = this.buildAbortSignal(downloadTimeout);\n\t\tconst response = await this.fetchZipResponse(\n\t\t\tbundleURL,\n\t\t\tsignal,\n\t\t\tdownloadTimeout,\n\t\t);\n\n\t\tconst body = this.getZipResponseBody(response, url);\n\n\t\tawait this.writeZipToDisk(body, tempZipPath);\n\n\t\treturn tempZipPath;\n\t}\n\n\t/**\n\t * Builds a unique temporary file path for storing the downloaded ZIP bundle.\n\t *\n\t * Uses a UUID when available or falls back to a combination of PID, timestamp, and random bytes.\n\t *\n\t * @returns A full path to a temporary ZIP file in the OS temp directory.\n\t */\n\tprotected buildTempZipPath(): string {\n\t\tconst uid =\n\t\t\tcrypto.randomUUID?.() ??\n\t\t\t`${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString(\"hex\")}`;\n\n\t\treturn path.join(os.tmpdir(), `lokalise-${uid}.zip`);\n\t}\n\n\t/**\n\t * Creates an optional AbortSignal for enforcing request timeouts.\n\t *\n\t * Returns `undefined` when no timeout is configured, disabling abort handling.\n\t *\n\t * @param downloadTimeout - Timeout in milliseconds. `0` or negative disables the signal.\n\t * @returns An AbortSignal if timeout is enabled, otherwise `undefined`.\n\t */\n\tprivate buildAbortSignal(downloadTimeout: number): AbortSignal | undefined {\n\t\tif (downloadTimeout <= 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn AbortSignal.timeout(downloadTimeout);\n\t}\n\n\t/**\n\t * Executes a fetch request for the ZIP bundle URL with optional timeout handling.\n\t *\n\t * Wraps network failures, timeouts, and unexpected fetch errors into `LokaliseError`\n\t * so higher-level logic receives consistent exceptions.\n\t *\n\t * @param bundleURL - Parsed URL pointing to the ZIP file.\n\t * @param signal - Optional `AbortSignal` used to enforce request timeouts.\n\t * @param downloadTimeout - Timeout duration (ms) used for error messaging.\n\t * @returns The raw `Response` object returned by `fetch` if the request succeeds.\n\t */\n\tprotected async fetchZipResponse(\n\t\tbundleURL: URL,\n\t\tsignal: AbortSignal | undefined,\n\t\tdownloadTimeout: number,\n\t): Promise<Response> {\n\t\ttry {\n\t\t\treturn await fetch(bundleURL, signal ? { signal } : {});\n\t\t} catch (err) {\n\t\t\tif (err instanceof Error) {\n\t\t\t\tif (err.name === \"TimeoutError\") {\n\t\t\t\t\tthrow new LokaliseError(\n\t\t\t\t\t\t`Request timed out after ${downloadTimeout}ms`,\n\t\t\t\t\t\t408,\n\t\t\t\t\t\t{ reason: \"timeout\" },\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tthrow new LokaliseError(err.message, 500, {\n\t\t\t\t\treason: \"network or fetch error\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// This should never happen in production\n\t\t\t// as realistically fetch always raises Error,\n\t\t\t// unless some black magic has been involved.\n\t\t\t/* v8 ignore start */\n\t\t\tthrow new LokaliseError(\n\t\t\t\t\"An unknown error occurred. This might indicate a bug.\",\n\t\t\t\t500,\n\t\t\t\t{\n\t\t\t\t\treason: String(err),\n\t\t\t\t},\n\t\t\t);\n\t\t\t/* v8 ignore end */\n\t\t}\n\t}\n\n\t/**\n\t * Validates and extracts the readable body stream from a fetch response.\n\t *\n\t * Ensures the response is OK and has a non-null body before returning it.\n\t *\n\t * @param response - The HTTP response returned by `fetch`.\n\t * @param originalUrl - Original URL used for error diagnostics.\n\t * @returns A web ReadableStream of the ZIP file contents.\n\t * @throws {LokaliseError} If the response is not OK or body is missing.\n\t */\n\tprivate getZipResponseBody(\n\t\tresponse: Response,\n\t\toriginalUrl: string,\n\t): WebReadableStream<Uint8Array> {\n\t\tif (!response.ok) {\n\t\t\tthrow new LokaliseError(\n\t\t\t\t`Failed to download ZIP file: ${response.statusText} (${response.status})`,\n\t\t\t);\n\t\t}\n\n\t\tconst body = response.body as WebReadableStream<Uint8Array> | null;\n\n\t\tif (!body) {\n\t\t\tthrow new LokaliseError(\n\t\t\t\t`Response body is null. Cannot download ZIP file from URL: ${originalUrl}`,\n\t\t\t);\n\t\t}\n\n\t\treturn body;\n\t}\n\n\t/**\n\t * Streams the ZIP response body to a temporary file on disk.\n\t *\n\t * Cleans up the temporary file if the streaming pipeline fails.\n\t *\n\t * @param body - Web readable stream of the ZIP content.\n\t * @param tempZipPath - Path where the ZIP should be written.\n\t * @returns A promise that resolves once the file is fully written.\n\t * @throws {Error} Re-throws any pipeline errors after attempting cleanup.\n\t */\n\tprivate async writeZipToDisk(\n\t\tbody: WebReadableStream<Uint8Array>,\n\t\ttempZipPath: string,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst nodeReadable = Readable.fromWeb(body);\n\t\t\tawait this.streamPipeline(\n\t\t\t\tnodeReadable,\n\t\t\t\tfs.createWriteStream(tempZipPath),\n\t\t\t);\n\t\t} catch (e) {\n\t\t\ttry {\n\t\t\t\tawait fs.promises.unlink(tempZipPath);\n\t\t\t} catch {\n\t\t\t\tthis.logMsg(\n\t\t\t\t\t\"debug\",\n\t\t\t\t\t`Stream pipeline failed and unable to remove temp path ${tempZipPath}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves a translation bundle from Lokalise with retries and exponential backoff.\n\t *\n\t * @param downloadFileParams - Parameters for Lokalise API file download.\n\t * @returns The downloaded bundle metadata.\n\t * @throws {LokaliseError} If retries are exhausted or an API error occurs.\n\t */\n\tprotected async getTranslationsBundle(\n\t\tdownloadFileParams: DownloadFileParams,\n\t): Promise<DownloadBundle> {\n\t\treturn this.withExponentialBackoff(() =>\n\t\t\tthis.apiClient.files().download(this.projectId, downloadFileParams),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieves a translation bundle from Lokalise with retries and exponential backoff.\n\t *\n\t * @param downloadFileParams - Parameters for Lokalise API file download.\n\t * @returns The queued process.\n\t * @throws {LokaliseError} If retries are exhausted or an API error occurs.\n\t */\n\tprotected async getTranslationsBundleAsync(\n\t\tdownloadFileParams: DownloadFileParams,\n\t): Promise<QueuedProcess> {\n\t\treturn this.withExponentialBackoff(() =>\n\t\t\tthis.apiClient.files().async_download(this.projectId, downloadFileParams),\n\t\t);\n\t}\n\n\t/**\n\t * Extracts a single entry from a ZIP archive to the specified output directory.\n\t *\n\t * Creates necessary directories and streams the file content to disk.\n\t *\n\t * @param entry - The ZIP entry to extract.\n\t * @param zipfile - The open ZIP file instance.\n\t * @param outputDir - The directory where the entry should be written.\n\t * @returns A promise that resolves when the entry is fully written.\n\t */\n\tprotected async handleZipEntry(\n\t\tentry: yauzl.Entry,\n\t\tzipfile: yauzl.ZipFile,\n\t\toutputDir: string,\n\t): Promise<void> {\n\t\tconst fullPath = this.processZipEntryPath(outputDir, entry.fileName);\n\n\t\tif (entry.fileName.endsWith(\"/\")) {\n\t\t\t// it's a directory\n\t\t\tawait this.createDir(fullPath);\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.createDir(path.dirname(fullPath));\n\n\t\treturn new Promise((response, reject) => {\n\t\t\tzipfile.openReadStream(entry, (readErr, readStream) => {\n\t\t\t\tif (readErr || !readStream) {\n\t\t\t\t\treturn reject(\n\t\t\t\t\t\tnew LokaliseError(`Failed to read ZIP entry: ${entry.fileName}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst writeStream = fs.createWriteStream(fullPath);\n\t\t\t\treadStream.pipe(writeStream);\n\t\t\t\twriteStream.on(\"finish\", response);\n\t\t\t\twriteStream.on(\"error\", reject);\n\t\t\t\treadStream.on(\"error\", reject);\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Creates a directory and all necessary parent directories.\n\t *\n\t * @param dir - The directory path to create.\n\t * @returns A promise that resolves when the directory is created.\n\t */\n\tprivate async createDir(dir: string): Promise<void> {\n\t\tawait fs.promises.mkdir(dir, { recursive: true });\n\t}\n\n\t/**\n\t * Resolves and validates the full output path for a ZIP entry.\n\t *\n\t * Prevents path traversal attacks by ensuring the resolved path stays within the output directory.\n\t *\n\t * @param outputDir - The base output directory.\n\t * @param entryFilename - The filename of the ZIP entry.\n\t * @returns The absolute and safe path to write the entry.\n\t * @throws {LokaliseError} If the entry path is detected as malicious.\n\t */\n\tprotected processZipEntryPath(\n\t\toutputDir: string,\n\t\tentryFilename: string,\n\t): string {\n\t\t// Validate paths to avoid path traversal issues\n\t\tconst fullPath = path.resolve(outputDir, entryFilename);\n\t\tconst relative = path.relative(outputDir, fullPath);\n\t\tif (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n\t\t\tthrow new LokaliseError(`Malicious ZIP entry detected: ${entryFilename}`);\n\t\t}\n\n\t\treturn fullPath;\n\t}\n\n\t/**\n\t * Parses and validates a URL string, ensuring it uses HTTP or HTTPS protocol.\n\t *\n\t * @param value - The URL string to validate.\n\t * @returns A parsed `URL` object if valid.\n\t * @throws {LokaliseError} If the URL is invalid or uses an unsupported protocol.\n\t */\n\tprivate assertHttpUrl(value: string): URL {\n\t\tlet parsed: URL;\n\t\ttry {\n\t\t\tparsed = new URL(value);\n\t\t} catch {\n\t\t\tthrow new LokaliseError(`Invalid URL: ${value}`);\n\t\t}\n\n\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\tthrow new LokaliseError(`Unsupported protocol in URL: ${value}`);\n\t\t}\n\n\t\treturn parsed;\n\t}\n\n\t/**\n\t * Builds effective process parameters for the download workflow.\n\t *\n\t * Merges caller-provided overrides with the default settings.\n\t *\n\t * @param overrides - Partial process configuration to override defaults.\n\t * @returns Fully resolved process parameters.\n\t */\n\tprivate buildProcessParams(\n\t\toverrides?: Partial<ProcessDownloadFileParams>,\n\t): Required<ProcessDownloadFileParams> {\n\t\treturn {\n\t\t\t...LokaliseDownload.defaultProcessParams,\n\t\t\t...overrides,\n\t\t};\n\t}\n\n\t/**\n\t * Unpacks the downloaded ZIP archive into the target directory and\n\t * removes the temporary archive file afterwards.\n\t *\n\t * Logs progress and always attempts to delete the temporary file.\n\t *\n\t * @param zipFilePath - Path to the temporary ZIP file.\n\t * @param unpackTo - Destination directory for extracted files.\n\t */\n\tprivate async processZip(\n\t\tzipFilePath: string,\n\t\tunpackTo: string,\n\t): Promise<void> {\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Unpacking translations from ${zipFilePath} to ${unpackTo}`,\n\t\t);\n\n\t\ttry {\n\t\t\tawait this.unpackZip(zipFilePath, unpackTo);\n\n\t\t\tthis.logMsg(\"debug\", \"Translations unpacked!\");\n\t\t\tthis.logMsg(\"debug\", \"Download successful!\");\n\t\t} finally {\n\t\t\tthis.logMsg(\"debug\", `Removing temp archive from ${zipFilePath}`);\n\t\t\tawait fs.promises.unlink(zipFilePath);\n\t\t}\n\t}\n\n\t/**\n\t * Fetches the direct bundle URL in synchronous (non-async) mode.\n\t *\n\t * Calls the standard download endpoint without polling.\n\t *\n\t * @param downloadFileParams - Parameters for Lokalise API file download.\n\t * @returns Direct bundle URL returned by Lokalise.\n\t */\n\tprivate async fetchBundleURLSync(\n\t\tdownloadFileParams: DownloadFileParams,\n\t): Promise<string> {\n\t\tthis.logMsg(\"debug\", \"Async download mode disabled.\");\n\n\t\tconst translationsBundle =\n\t\t\tawait this.getTranslationsBundle(downloadFileParams);\n\t\treturn translationsBundle.bundle_url;\n\t}\n\n\t/**\n\t * Polls an async download process until it completes or the maximum wait time is reached.\n\t *\n\t * Validates the final status and throws if the process did not finish properly.\n\t *\n\t * @param downloadProcess - The initially queued async process.\n\t * @param initialWait - Initial interval in ms before the first poll.\n\t * @param maxWait - Maximum total wait time in ms.\n\t * @returns The completed process object.\n\t * @throws {LokaliseError} If the process is not found or does not finish successfully.\n\t */\n\tprotected async pollAsyncDownload(\n\t\tdownloadProcess: QueuedProcess,\n\t\tinitialWait: number,\n\t\tmaxWait: number,\n\t): Promise<QueuedProcess> {\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Waiting for download process ID ${downloadProcess.process_id} to complete...`,\n\t\t);\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Effective waits: initial=${initialWait}ms, max=${maxWait}ms`,\n\t\t);\n\n\t\tconst results = await this.pollProcesses(\n\t\t\t[downloadProcess],\n\t\t\tinitialWait,\n\t\t\tmaxWait,\n\t\t);\n\n\t\tconst completedProcess = results.find(\n\t\t\t(p) => p.process_id === downloadProcess.process_id,\n\t\t);\n\n\t\tif (!completedProcess) {\n\t\t\tthrow new LokaliseError(\n\t\t\t\t`Process ${downloadProcess.process_id} not found after polling`,\n\t\t\t\t500,\n\t\t\t);\n\t\t}\n\n\t\tif (!LokaliseFileExchange.isFinishedStatus(completedProcess.status)) {\n\t\t\tthrow new LokaliseError(\n\t\t\t\t`Download process did not finish within ${maxWait}ms` +\n\t\t\t\t\t`${completedProcess.status ? ` (last status=${completedProcess.status})` : \" (status missing)\"}`,\n\t\t\t\t504,\n\t\t\t);\n\t\t}\n\n\t\treturn completedProcess;\n\t}\n\n\t/**\n\t * Resolves the bundle download URL using either async or sync strategy.\n\t *\n\t * Delegates to `fetchBundleURLAsync` or `fetchBundleURLSync`\n\t * based on the `asyncDownload` flag.\n\t *\n\t * @param downloadFileParams - Parameters for Lokalise API file download.\n\t * @param processParams - Effective process parameters controlling async behavior and polling.\n\t * @returns Direct bundle URL to download.\n\t */\n\tprivate fetchTranslationBundleURL(\n\t\tdownloadFileParams: DownloadFileParams,\n\t\tprocessParams: Required<ProcessDownloadFileParams>,\n\t): Promise<string> {\n\t\treturn processParams.asyncDownload\n\t\t\t? this.fetchBundleURLAsync(downloadFileParams, processParams)\n\t\t\t: this.fetchBundleURLSync(downloadFileParams);\n\t}\n\n\t/**\n\t * Extracts and verifies the download URL from a finished async process.\n\t *\n\t * Ensures `details.download_url` is present and is a string.\n\t *\n\t * @param completedProcess - Process object with status `finished`.\n\t * @returns Valid download URL string.\n\t * @throws {LokaliseError} If the URL is missing or invalid.\n\t */\n\tprivate handleFinishedAsyncProcess(completedProcess: QueuedProcess): string {\n\t\tconst details = completedProcess.details as\n\t\t\t| (DownloadedFileProcessDetails & { download_url?: string })\n\t\t\t| undefined;\n\n\t\tconst url = details?.download_url;\n\t\tif (!url || typeof url !== \"string\") {\n\t\t\tthis.logMsg(\n\t\t\t\t\"warn\",\n\t\t\t\t\"Process finished but details.download_url is missing or invalid\",\n\t\t\t\tdetails,\n\t\t\t);\n\t\t\tthrow new LokaliseError(\n\t\t\t\t\"Lokalise returned finished process without a valid download_url\",\n\t\t\t\t502,\n\t\t\t);\n\t\t}\n\n\t\treturn url;\n\t}\n\n\t/**\n\t * Handles a failed or cancelled async process by throwing an error with context.\n\t *\n\t * Includes the process status and optional message from Lokalise.\n\t *\n\t * @param completedProcess - Process object with status `failed` or `cancelled`.\n\t * @throws {LokaliseError} Always throws, as the process did not succeed.\n\t */\n\tprivate handleFailedAsyncProcess(completedProcess: QueuedProcess): never {\n\t\tconst msg = completedProcess.message?.trim();\n\t\tthrow new LokaliseError(\n\t\t\t`Process ${completedProcess.process_id} ended with status=${completedProcess.status}` +\n\t\t\t\t(msg ? `: ${msg}` : \"\"),\n\t\t\t502,\n\t\t);\n\t}\n\n\t/**\n\t * Handles an unexpected async process outcome when it did not finish in time.\n\t *\n\t * Logs a warning and throws an error indicating that finalization took too long.\n\t *\n\t * @param completedProcess - Process object with unexpected status.\n\t * @param maxWait - Effective maximum wait time used during polling.\n\t * @throws {LokaliseError} Always throws to signal an unexpected async outcome.\n\t */\n\tprivate handleUnexpectedAsyncProcess(\n\t\tcompletedProcess: QueuedProcess,\n\t\tmaxWait: number,\n\t): never {\n\t\tthis.logMsg(\"warn\", `Process ended with status=${completedProcess.status}`);\n\t\tthrow new LokaliseError(\n\t\t\t`Download process took too long to finalize; effective=${maxWait}ms`,\n\t\t\t500,\n\t\t);\n\t}\n\n\t/**\n\t * Runs the async download flow: queues the download, polls its status,\n\t * and returns the final bundle URL once the process completes.\n\t *\n\t * Handles finished, failed/cancelled, and unexpected statuses separately.\n\t *\n\t * @param downloadFileParams - Parameters for Lokalise API async file download.\n\t * @param processParams - Effective process parameters controlling polling behavior.\n\t * @returns Direct URL to the generated ZIP bundle.\n\t * @throws {LokaliseError} If the process fails, is cancelled, or does not finalize properly.\n\t */\n\tprotected async fetchBundleURLAsync(\n\t\tdownloadFileParams: DownloadFileParams,\n\t\tprocessParams: Required<ProcessDownloadFileParams>,\n\t): Promise<string> {\n\t\tthis.logMsg(\"debug\", \"Async download mode enabled.\");\n\n\t\tconst downloadProcess =\n\t\t\tawait this.getTranslationsBundleAsync(downloadFileParams);\n\n\t\tconst { pollInitialWaitTime, pollMaximumWaitTime } = processParams;\n\n\t\tconst completedProcess = await this.pollAsyncDownload(\n\t\t\tdownloadProcess,\n\t\t\tpollInitialWaitTime,\n\t\t\tpollMaximumWaitTime,\n\t\t);\n\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t`Download process status is ${completedProcess.status}`,\n\t\t);\n\n\t\tif (completedProcess.status === \"finished\") {\n\t\t\treturn this.handleFinishedAsyncProcess(completedProcess);\n\t\t}\n\n\t\tif (\n\t\t\tcompletedProcess.status === \"failed\" ||\n\t\t\tcompletedProcess.status === \"cancelled\"\n\t\t) {\n\t\t\tthis.handleFailedAsyncProcess(completedProcess);\n\t\t}\n\n\t\tthis.handleUnexpectedAsyncProcess(completedProcess, pollMaximumWaitTime);\n\t}\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { QueuedProcess, UploadFileParams } from \"@lokalise/node-api\";\nimport type {\n\tCollectFileParams,\n\tFileUploadError,\n\tPartialUploadFileParams,\n\tProcessedFile,\n\tProcessUploadFileParams,\n\tQueuedUploadProcessesWithErrors,\n\tUploadTranslationParams,\n} from \"../interfaces/index.js\";\nimport { LokaliseFileExchange } from \"./LokaliseFileExchange.js\";\n\n/**\n * Handles uploading translation files to Lokalise.\n */\nexport class LokaliseUpload extends LokaliseFileExchange {\n\tprivate static readonly defaultPollingParams = {\n\t\tpollStatuses: false,\n\t\tpollInitialWaitTime: 1000,\n\t\tpollMaximumWaitTime: 120_000,\n\t};\n\n\t/**\n\t * Collects files, uploads them to Lokalise, and optionally polls for process completion, returning both processes and errors.\n\t *\n\t * @param {UploadTranslationParams} uploadTranslationParams - Parameters for collecting and uploading files.\n\t * @returns {Promise<{ processes: QueuedProcess[]; errors: FileUploadError[] }>} A promise resolving with successful processes and upload errors.\n\t */\n\tasync uploadTranslations({\n\t\tuploadFileParams,\n\t\tcollectFileParams,\n\t\tprocessUploadFileParams,\n\t}: UploadTranslationParams = {}): Promise<QueuedUploadProcessesWithErrors> {\n\t\tthis.logMsg(\"debug\", \"Uploading translations to Lokalise...\");\n\n\t\tconst { pollStatuses, pollInitialWaitTime, pollMaximumWaitTime } = {\n\t\t\t...LokaliseUpload.defaultPollingParams,\n\t\t\t...processUploadFileParams,\n\t\t};\n\n\t\tthis.logMsg(\"debug\", \"Collecting files to upload...\");\n\t\tconst collectedFiles = await this.collectFiles(collectFileParams);\n\t\tthis.logMsg(\"debug\", \"Collected files:\", collectedFiles);\n\n\t\tthis.logMsg(\"debug\", \"Performing parallel upload...\");\n\t\tconst { processes, errors } = await this.parallelUpload(\n\t\t\tcollectedFiles,\n\t\t\tuploadFileParams,\n\t\t\tprocessUploadFileParams,\n\t\t);\n\n\t\tlet completedProcesses = processes;\n\t\tthis.logMsg(\n\t\t\t\"debug\",\n\t\t\t\"File uploading queued! IDs:\",\n\t\t\tcompletedProcesses.map((p) => p.process_id),\n\t\t);\n\n\t\tif (pollStatuses) {\n\t\t\tthis.logMsg(\"debug\", \"Polling queued processes...\");\n\n\t\t\tcompletedProcesses = await this.pollProcesses(\n\t\t\t\tprocesses,\n\t\t\t\tpollInitialWaitTime,\n\t\t\t\tpollMaximumWaitTime,\n\t\t\t);\n\n\t\t\tthis.logMsg(\"debug\", \"Polling completed!\");\n\t\t}\n\n\t\tthis.logMsg(\"debug\", \"Upload successful!\");\n\n\t\treturn { processes: completedProcesses, errors };\n\t}\n\n\t/**\n\t * Collects files from the filesystem based on the given parameters.\n\t *\n\t * @param {CollectFileParams} collectFileParams - Parameters for file collection, including directories, extensions, and patterns.\n\t * @returns {Promise<string[]>} A promise resolving with the list of collected file paths.\n\t */\n\tprotected async collectFiles({\n\t\tinputDirs = [\"./locales\"],\n\t\textensions = [\".*\"],\n\t\texcludePatterns = [],\n\t\trecursive = true,\n\t\tfileNamePattern = \".*\",\n\t}: CollectFileParams = {}): Promise<string[]> {\n\t\tconst queue = this.makeQueue(inputDirs);\n\t\tconst normalizedExtensions = this.normalizeExtensions(extensions);\n\t\tconst fileNameRegex = this.makeFilenameRegexp(fileNamePattern);\n\t\tconst excludeRegexes = this.makeExcludeRegExes(excludePatterns);\n\n\t\tconst files = await this.processCollectionQueue(\n\t\t\tqueue,\n\t\t\tnormalizedExtensions,\n\t\t\tfileNameRegex,\n\t\t\texcludeRegexes,\n\t\t\trecursive,\n\t\t);\n\n\t\treturn files.sort();\n\t}\n\n\t/**\n\t * Uploads a single file to Lokalise.\n\t *\n\t * @param {UploadFileParams} uploadParams - Parameters for uploading the file.\n\t * @returns {Promise<QueuedProcess>} A promise resolving with the upload process details.\n\t */\n\tprotected async uploadSingleFile(\n\t\tuploadParams: UploadFileParams,\n\t): Promise<QueuedProcess> {\n\t\treturn this.withExponentialBackoff(() =>\n\t\t\tthis.apiClient.files().upload(this.projectId, uploadParams),\n\t\t);\n\t}\n\n\t/**\n\t * Processes a file to prepare it for upload, converting it to base64 and extracting its language code.\n\t *\n\t * @param file - The absolute path to the file.\n\t * @param projectRoot - The root directory of the project.\n\t * @param processParams - Optional processing settings including inferers.\n\t * @returns A promise resolving with the processed file details, including base64 content, relative path, and language code.\n\t */\n\tprotected async processFile(\n\t\tfile: string,\n\t\tprojectRoot: string,\n\t\tprocessParams?: ProcessUploadFileParams,\n\t): Promise<ProcessedFile> {\n\t\tconst relativePath = await this.inferRelativePath(\n\t\t\tfile,\n\t\t\tprojectRoot,\n\t\t\tprocessParams,\n\t\t);\n\n\t\tconst languageCode = await this.inferLanguageCode(\n\t\t\tfile,\n\t\t\trelativePath,\n\t\t\tprocessParams,\n\t\t);\n\n\t\tconst base64Content = await this.readFileAsBase64(file);\n\n\t\treturn {\n\t\t\tdata: base64Content,\n\t\t\tfilename: relativePath,\n\t\t\tlang_iso: languageCode,\n\t\t};\n\t}\n\n\t/**\n\t * Infers the relative path for an uploaded file.\n\t *\n\t * Tries a custom `filenameInferer` first; if it fails or returns empty/whitespace,\n\t * falls back to a POSIX-style relative path based on the project root.\n\t *\n\t * @param file - Absolute path to the source file.\n\t * @param projectRoot - Root directory of the project.\n\t * @param processParams - Optional processing settings including filename inferer.\n\t * @returns A promise resolving with the inferred relative path.\n\t */\n\tprivate async inferRelativePath(\n\t\tfile: string,\n\t\tprojectRoot: string,\n\t\tprocessParams?: ProcessUploadFileParams,\n\t): Promise<string> {\n\t\ttry {\n\t\t\tconst fromInferer = processParams?.filenameInferer\n\t\t\t\t? await processParams.filenameInferer(file)\n\t\t\t\t: \"\";\n\n\t\t\tif (!fromInferer.trim()) {\n\t\t\t\tthrow new Error(\"Invalid filename: empty or only whitespace\");\n\t\t\t}\n\n\t\t\treturn fromInferer;\n\t\t} catch {\n\t\t\t// Fallback: derive a POSIX relative path from project root\n\t\t\treturn path.posix.relative(\n\t\t\t\tthis.toPosixPath(projectRoot),\n\t\t\t\tthis.toPosixPath(file),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Infers the language code for an uploaded file.\n\t *\n\t * Tries a custom `languageInferer` first; if it fails or returns empty/whitespace,\n\t * falls back to extracting the language code from the filename before the last extension.\n\t *\n\t * Example: \"en.default.json\" → \"default\"\n\t *\n\t * @param file - Absolute path to the source file.\n\t * @param relativePath - Effective relative path of the file (used for fallback parsing).\n\t * @param processParams - Optional processing settings including language inferer.\n\t * @returns A promise resolving with the inferred language code.\n\t */\n\tprivate async inferLanguageCode(\n\t\tfile: string,\n\t\trelativePath: string,\n\t\tprocessParams?: ProcessUploadFileParams,\n\t): Promise<string> {\n\t\ttry {\n\t\t\tconst fromInferer = processParams?.languageInferer\n\t\t\t\t? await processParams.languageInferer(file)\n\t\t\t\t: \"\";\n\n\t\t\tif (!fromInferer.trim()) {\n\t\t\t\tthrow new Error(\"Invalid language code: empty or only whitespace\");\n\t\t\t}\n\n\t\t\treturn fromInferer;\n\t\t} catch {\n\t\t\t// Fallback: derive language code from the basename of the relative path\n\t\t\tconst baseName = path.basename(relativePath);\n\t\t\treturn baseName.split(\".\").slice(-2, -1)[0] ?? \"unknown\";\n\t\t}\n\t}\n\n\t/**\n\t * Reads a file from disk and returns its content encoded as base64.\n\t *\n\t * @param file - Absolute path to the source file.\n\t * @returns A promise resolving with the file content encoded in base64.\n\t */\n\tprivate async readFileAsBase64(file: string): Promise<string> {\n\t\tconst fileContent = await fs.promises.readFile(file);\n\t\treturn fileContent.toString(\"base64\");\n\t}\n\n\t/**\n\t * Uploads files in parallel with a limit on the number of concurrent uploads.\n\t *\n\t * @param {string[]} files - List of file paths to upload.\n\t * @param {Partial<UploadFileParams>} baseUploadFileParams - Base parameters for uploads.\n\t * @param {ProcessUploadFileParams} [processParams] - Optional processing settings including inferers.\n\t * @returns {Promise<{ processes: QueuedProcess[]; errors: FileUploadError[] }>} A promise resolving with successful processes and upload errors.\n\t */\n\tprivate async parallelUpload(\n\t\tfiles: string[],\n\t\tbaseUploadFileParams: PartialUploadFileParams = {},\n\t\tprocessParams?: ProcessUploadFileParams,\n\t): Promise<QueuedUploadProcessesWithErrors> {\n\t\tconst projectRoot = process.cwd();\n\t\tconst queuedProcesses: QueuedProcess[] = [];\n\t\tconst errors: FileUploadError[] = [];\n\n\t\tawait this.runWithConcurrencyLimit(\n\t\t\tfiles,\n\t\t\tLokaliseUpload.maxConcurrentProcesses,\n\t\t\tasync (file) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst processedFileParams = await this.processFile(\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tprojectRoot,\n\t\t\t\t\t\tprocessParams,\n\t\t\t\t\t);\n\t\t\t\t\tconst queued = await this.uploadSingleFile({\n\t\t\t\t\t\t...baseUploadFileParams,\n\t\t\t\t\t\t...processedFileParams,\n\t\t\t\t\t});\n\t\t\t\t\tqueuedProcesses.push(queued);\n\t\t\t\t} catch (error) {\n\t\t\t\t\terrors.push({ file, error });\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\treturn { processes: queuedProcesses, errors };\n\t}\n\n\t/**\n\t * Normalizes an array of file extensions by ensuring each starts with a dot and is lowercase.\n\t *\n\t * @param extensions - The list of file extensions to normalize.\n\t * @returns A new array with normalized file extensions.\n\t */\n\tprivate normalizeExtensions(extensions: string[]): string[] {\n\t\treturn extensions.map((ext) =>\n\t\t\t(ext.startsWith(\".\") ? ext : `.${ext}`).toLowerCase(),\n\t\t);\n\t}\n\n\t/**\n\t * Determines whether a file should be collected based on its extension and name pattern.\n\t *\n\t * @param entry - The directory entry to evaluate.\n\t * @param normalizedExtensions - List of allowed file extensions.\n\t * @param fileNameRegex - Regular expression to match valid filenames.\n\t * @returns `true` if the file matches both extension and name pattern, otherwise `false`.\n\t */\n\tprivate shouldCollectFile(\n\t\tentry: fs.Dirent,\n\t\tnormalizedExtensions: string[],\n\t\tfileNameRegex: RegExp,\n\t): boolean {\n\t\tconst fileExt = path.extname(entry.name).toLowerCase();\n\t\tconst matchesExtension =\n\t\t\tnormalizedExtensions.includes(\".*\") ||\n\t\t\tnormalizedExtensions.includes(fileExt);\n\t\tconst matchesFilenamePattern = fileNameRegex.test(entry.name);\n\n\t\treturn matchesExtension && matchesFilenamePattern;\n\t}\n\n\t/**\n\t * Creates a regular expression from a given pattern string or RegExp.\n\t *\n\t * @param fileNamePattern - The filename pattern to convert into a RegExp.\n\t * @returns A valid RegExp object.\n\t * @throws {Error} If the pattern string is invalid and cannot be compiled.\n\t */\n\tprivate makeFilenameRegexp(fileNamePattern: string | RegExp): RegExp {\n\t\ttry {\n\t\t\treturn new RegExp(fileNamePattern);\n\t\t} catch {\n\t\t\tthrow new Error(`Invalid fileNamePattern: ${fileNamePattern}`);\n\t\t}\n\t}\n\n\t/**\n\t * Converts an array of exclude patterns into an array of RegExp objects.\n\t *\n\t * @param excludePatterns - An array of strings or regular expressions to exclude.\n\t * @returns An array of compiled RegExp objects.\n\t * @throws {Error} If any pattern is invalid and cannot be compiled.\n\t */\n\tprivate makeExcludeRegExes(excludePatterns: string[] | RegExp[]): RegExp[] {\n\t\tif (excludePatterns.length === 0) {\n\t\t\treturn [];\n\t\t}\n\t\ttry {\n\t\t\treturn excludePatterns.map((pattern) => new RegExp(pattern));\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tthrow new Error(`Invalid excludePatterns: ${msg}`);\n\t\t}\n\t}\n\n\t/**\n\t * Safely reads the contents of a directory, returning an empty array if access fails.\n\t *\n\t * Logs a warning if the directory cannot be read (e.g. due to permissions or non-existence).\n\t *\n\t * @param dir - The directory path to read.\n\t * @returns A promise that resolves to an array of directory entries, or an empty array on failure.\n\t */\n\tprivate async safeReadDir(dir: string): Promise<fs.Dirent[]> {\n\t\ttry {\n\t\t\treturn await fs.promises.readdir(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tthis.logMsg(\"warn\", `Skipping inaccessible directory: ${dir}...`);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Checks if a file path matches any of the provided exclusion patterns.\n\t *\n\t * @param filePath - The path of the file to check.\n\t * @param excludeRegexes - An array of RegExp patterns to test against.\n\t * @returns `true` if the file path matches any exclude pattern, otherwise `false`.\n\t */\n\tprivate shouldExclude(filePath: string, rx: RegExp[]): boolean {\n\t\tconst posix = this.toPosixPath(filePath);\n\t\treturn rx.some((r) => r.test(filePath) || r.test(posix));\n\t}\n\n\t/**\n\t * Creates a queue of absolute paths from the provided input directories.\n\t *\n\t * @param inputDirs - An array of input directory paths (relative or absolute).\n\t * @returns An array of resolved absolute directory paths.\n\t */\n\tprivate makeQueue(inputDirs: string[]): string[] {\n\t\treturn [...inputDirs.map((dir) => path.resolve(dir))];\n\t}\n\n\t/**\n\t * Processes a queue of directories to collect files matching given criteria.\n\t *\n\t * Recursively reads directories (if enabled), filters files by extension,\n\t * filename pattern, and exclusion rules, and collects matching file paths.\n\t *\n\t * @param queue - The list of directories to process.\n\t * @param exts - Allowed file extensions (normalized).\n\t * @param nameRx - Regular expression to match valid filenames.\n\t * @param excludeRx - Array of exclusion patterns.\n\t * @param recursive - Whether to traverse subdirectories.\n\t * @returns A promise that resolves to an array of matched file paths.\n\t */\n\tprivate async processCollectionQueue(\n\t\tqueue: string[],\n\t\texts: string[],\n\t\tnameRx: RegExp,\n\t\texcludeRx: RegExp[],\n\t\trecursive: boolean,\n\t): Promise<string[]> {\n\t\tconst found: string[] = [];\n\n\t\twhile (queue.length) {\n\t\t\tconst dir = queue.shift();\n\n\t\t\t// Realistically, this should never happen:\n\t\t\t/* v8 ignore start */\n\t\t\tif (!dir) {\n\t\t\t\tthis.logMsg(\n\t\t\t\t\t\"debug\",\n\t\t\t\t\t`collectFiles: received falsy dir entry (${String(dir)}). This is unexpected and might indicate a bug.`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* v8 ignore stop */\n\n\t\t\tconst entries = await this.safeReadDir(dir);\n\t\t\tfor (const entry of entries) {\n\t\t\t\tconst fullPath = path.resolve(dir, entry.name);\n\t\t\t\tthis.handleEntry(entry, fullPath, queue, found, {\n\t\t\t\t\texts,\n\t\t\t\t\tnameRx,\n\t\t\t\t\texcludeRx,\n\t\t\t\t\trecursive,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn found;\n\t}\n\n\t/**\n\t * Handles a single directory entry during file collection.\n\t *\n\t * Applies exclusion rules, optionally queues directories for recursion,\n\t * and collects files that match the specified extension and filename pattern.\n\t *\n\t * @param entry - The directory entry to handle.\n\t * @param fullPath - The absolute path to the entry.\n\t * @param queue - The processing queue for directories.\n\t * @param found - The list to store matched file paths.\n\t * @param opts - Options including extensions, name pattern, exclusions, and recursion flag.\n\t */\n\tprivate handleEntry(\n\t\tentry: fs.Dirent,\n\t\tfullPath: string,\n\t\tqueue: string[],\n\t\tfound: string[],\n\t\topts: {\n\t\t\texts: string[];\n\t\t\tnameRx: RegExp;\n\t\t\texcludeRx: RegExp[];\n\t\t\trecursive: boolean;\n\t\t},\n\t): void {\n\t\tif (this.shouldExclude(fullPath, opts.excludeRx)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (entry.isDirectory()) {\n\t\t\tif (opts.recursive) {\n\t\t\t\tqueue.push(fullPath);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tentry.isFile() &&\n\t\t\tthis.shouldCollectFile(entry, opts.exts, opts.nameRx)\n\t\t) {\n\t\t\tfound.push(fullPath);\n\t\t}\n\t}\n\n\t/**\n\t * Normalizes a filesystem path to POSIX format.\n\t *\n\t * Replaces platform-specific separators (e.g. `\\` on Windows)\n\t * with POSIX-style `/` to ensure consistent path handling\n\t * across different operating systems.\n\t *\n\t * @param p - Original filesystem path.\n\t * @returns The same path but with POSIX separators.\n\t */\n\tprivate toPosixPath(p: string): string {\n\t\treturn p.split(path.sep).join(path.posix.sep);\n\t}\n}\n"],"mappings":"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"}