locai 2.6.1 → 2.7.0

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.
Files changed (131) hide show
  1. package/README.md +23 -25
  2. package/dist/cli/helpers.d.ts +34 -2
  3. package/dist/cli/helpers.js +96 -37
  4. package/dist/cli.js +40 -39
  5. package/dist/commands/review.d.ts +33 -8
  6. package/dist/commands/review.js +89 -22
  7. package/dist/commands/translator.d.ts +4 -2
  8. package/dist/commands/translator.js +1 -1
  9. package/dist/config/index.d.ts +53 -66
  10. package/dist/config/index.js +2 -1
  11. package/dist/config/setup.d.ts +3 -2
  12. package/dist/config/setup.js +5 -3
  13. package/dist/core/adapters/android-adapter.d.ts +3 -2
  14. package/dist/core/adapters/android-adapter.js +12 -13
  15. package/dist/core/adapters/base-adapter.d.ts +13 -2
  16. package/dist/core/adapters/ios-adapter.d.ts +3 -2
  17. package/dist/core/adapters/json-adapter.d.ts +4 -3
  18. package/dist/core/adapters/json-adapter.js +1 -3
  19. package/dist/core/adapters/po-adapter.d.ts +3 -2
  20. package/dist/core/adapters/po-adapter.js +4 -5
  21. package/dist/core/adapters/properties-adapter.d.ts +3 -2
  22. package/dist/core/adapters/properties-adapter.js +2 -4
  23. package/dist/core/adapters/yaml-adapter.d.ts +4 -3
  24. package/dist/core/adapters/yaml-adapter.js +1 -3
  25. package/dist/core/context-processor.d.ts +21 -6
  26. package/dist/core/context-processor.js +2 -7
  27. package/dist/core/detection/framework-detector.js +1 -1
  28. package/dist/core/detection/structure-detector.js +1 -1
  29. package/dist/core/fallback-provider.d.ts +3 -4
  30. package/dist/core/fallback-provider.js +15 -21
  31. package/dist/core/orchestrator.d.ts +98 -26
  32. package/dist/core/orchestrator.js +56 -116
  33. package/dist/core/pipeline/Pipeline.d.ts +2 -2
  34. package/dist/core/pipeline/Pipeline.js +1 -3
  35. package/dist/core/pipeline/context.d.ts +69 -13
  36. package/dist/core/pipeline/context.js +1 -7
  37. package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
  38. package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
  39. package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
  40. package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
  41. package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
  42. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
  43. package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
  44. package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
  45. package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
  46. package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
  47. package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
  48. package/dist/core/pipeline/steps/TranslationStep.js +21 -23
  49. package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
  50. package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
  51. package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
  52. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
  53. package/dist/core/provider-factory.d.ts +15 -9
  54. package/dist/core/provider-factory.js +42 -32
  55. package/dist/providers/anthropic.d.ts +5 -4
  56. package/dist/providers/anthropic.js +3 -2
  57. package/dist/providers/api-types.d.ts +74 -0
  58. package/dist/providers/api-types.js +5 -0
  59. package/dist/providers/base-provider.d.ts +27 -31
  60. package/dist/providers/base-provider.js +22 -46
  61. package/dist/providers/dashscope.js +0 -2
  62. package/dist/providers/deepseek.js +0 -1
  63. package/dist/providers/gemini.js +0 -1
  64. package/dist/providers/openai.d.ts +3 -3
  65. package/dist/providers/openai.js +0 -1
  66. package/dist/providers/xai.js +0 -1
  67. package/dist/services/embedding-provider.js +4 -2
  68. package/dist/services/language-processor.d.ts +5 -3
  69. package/dist/services/language-processor.js +38 -43
  70. package/dist/services/source-analyzer.d.ts +24 -0
  71. package/dist/services/source-analyzer.js +161 -68
  72. package/dist/services/translation-service.d.ts +31 -13
  73. package/dist/services/translation-service.js +116 -71
  74. package/dist/services/vector-store.d.ts +15 -2
  75. package/dist/services/vector-store.js +14 -10
  76. package/dist/types/index.d.ts +31 -33
  77. package/dist/utils/ai-context-analyzer.d.ts +12 -3
  78. package/dist/utils/ai-context-analyzer.js +18 -10
  79. package/dist/utils/confidence-scorer.d.ts +16 -19
  80. package/dist/utils/confidence-scorer.js +36 -125
  81. package/dist/utils/error-helper.d.ts +27 -6
  82. package/dist/utils/error-helper.js +10 -9
  83. package/dist/utils/file-manager.d.ts +8 -7
  84. package/dist/utils/file-manager.js +22 -23
  85. package/dist/utils/glossary-manager.d.ts +22 -3
  86. package/dist/utils/glossary-manager.js +47 -28
  87. package/dist/utils/graceful-shutdown.d.ts +7 -1
  88. package/dist/utils/graceful-shutdown.js +1 -2
  89. package/dist/utils/input-validator.d.ts +53 -66
  90. package/dist/utils/input-validator.js +100 -151
  91. package/dist/utils/locale-paths.d.ts +7 -0
  92. package/dist/utils/locale-paths.js +16 -0
  93. package/dist/utils/logger.d.ts +8 -8
  94. package/dist/utils/logger.js +7 -15
  95. package/dist/utils/object-transformer.d.ts +20 -5
  96. package/dist/utils/object-transformer.js +20 -28
  97. package/dist/utils/progress-tracker.js +2 -3
  98. package/dist/utils/prompt-templates.d.ts +45 -5
  99. package/dist/utils/prompt-templates.js +61 -5
  100. package/dist/utils/quality/base-checker.d.ts +40 -18
  101. package/dist/utils/quality/base-checker.js +1 -2
  102. package/dist/utils/quality/code-block-checker.d.ts +13 -5
  103. package/dist/utils/quality/index.d.ts +2 -1
  104. package/dist/utils/quality/index.js +8 -27
  105. package/dist/utils/quality/length-checker.d.ts +35 -5
  106. package/dist/utils/quality/length-checker.js +2 -3
  107. package/dist/utils/quality/markdown-checker.d.ts +14 -7
  108. package/dist/utils/quality/special-characters-checker.d.ts +18 -1
  109. package/dist/utils/quality/special-characters-checker.js +0 -1
  110. package/dist/utils/quality/style-guide-checker.d.ts +1 -1
  111. package/dist/utils/quality/style-guide-checker.js +0 -2
  112. package/dist/utils/quality/tone-verifier.js +1 -1
  113. package/dist/utils/rate-limiter.d.ts +17 -1
  114. package/dist/utils/rate-limiter.js +12 -11
  115. package/dist/utils/retry-helper.d.ts +21 -3
  116. package/dist/utils/retry-helper.js +11 -3
  117. package/dist/utils/review-queue.d.ts +45 -0
  118. package/dist/utils/review-queue.js +53 -0
  119. package/dist/utils/state-manager.d.ts +12 -3
  120. package/dist/utils/state-manager.js +32 -7
  121. package/dist/utils/statistics-manager.d.ts +11 -1
  122. package/dist/utils/statistics-manager.js +8 -0
  123. package/dist/utils/ui-manager.d.ts +2 -1
  124. package/dist/utils/ui-manager.js +15 -1
  125. package/package.json +22 -17
  126. package/dist/commands/interactive-review.d.ts +0 -9
  127. package/dist/commands/interactive-review.js +0 -109
  128. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
  129. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
  130. package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
  131. package/dist/utils/quality/__tests__/tone-verifier.test.js +0 -91
@@ -3,8 +3,16 @@
3
3
  * Defines regex patterns, limits, and validation methods.
4
4
  */
5
5
  import path from "path";
6
+ const isObject = (value) => typeof value === "object" && value !== null;
7
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
6
8
  class InputValidator {
7
- static LANGUAGE_CODE_PATTERN = /^[a-z]{2}(-[a-z]{2})?$/;
9
+ /**
10
+ * Well-formed BCP 47 language tag, limited to the subtags locai supports:
11
+ * a 2-3 letter language, an optional 4-letter script and an optional
12
+ * 2-letter or 3-digit region (e.g. "en", "fil", "pt-BR", "zh-Hant-TW", "es-419").
13
+ * Matching is case-insensitive; see validateLanguageCode for canonical casing.
14
+ */
15
+ static LANGUAGE_CODE_PATTERN = /^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}|\d{3}))?$/i;
8
16
  static VALID_PROVIDERS = [
9
17
  "dashscope",
10
18
  "xai",
@@ -17,44 +25,43 @@ class InputValidator {
17
25
  static MAX_KEY_LENGTH = 500;
18
26
  static MAX_PATH_LENGTH = 1000;
19
27
  static MAX_CONFIG_DEPTH = 10;
20
- static DANGEROUS_PATTERNS = [
21
- /\.\.\//g,
22
- /\0/g,
23
- /[\x00-\x08\x0b\x0c\x0e-\x1f\x7F]/g, // eslint-disable-line no-control-regex
24
- /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
25
- /javascript:/gi,
26
- /data:.*base64/gi,
27
- ];
28
- static _requestCounts;
29
28
  /**
30
- * Validate and sanitize language code.
31
- * @param {string} langCode - Language code to validate.
29
+ * Validate a BCP 47 language tag and return it in canonical casing:
30
+ * lowercase language, title-case script, uppercase region
31
+ * (e.g. "ZH-hant-tw" -> "zh-Hant-TW", "PT-br" -> "pt-BR").
32
+ * @param {unknown} langCode - Language tag to validate.
32
33
  * @param {string} [paramName="language"] - Parameter name for error messages.
33
- * @returns {string} - Sanitized language code.
34
+ * @returns {string} - Canonical language tag.
34
35
  * @throws {Error} If validation fails.
35
36
  */
36
37
  static validateLanguageCode(langCode, paramName = "language") {
37
38
  if (!langCode || typeof langCode !== "string") {
38
39
  throw new Error(`${paramName} must be a non-empty string`);
39
40
  }
40
- const sanitized = langCode.trim().toLowerCase();
41
- if (sanitized.length === 0) {
41
+ const trimmed = langCode.trim();
42
+ if (trimmed.length === 0) {
42
43
  throw new Error(`${paramName} cannot be empty after trimming`);
43
44
  }
44
- if (sanitized.length > 10) {
45
- throw new Error(`${paramName} code too long: '${langCode}' (max 10 characters)`);
46
- }
47
- if (sanitized.includes("..") || sanitized.includes("/") || sanitized.includes("\\")) {
45
+ if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
48
46
  throw new Error(`${paramName} code contains forbidden characters: '${langCode}'`);
49
47
  }
50
- if (!this.LANGUAGE_CODE_PATTERN.test(sanitized)) {
51
- throw new Error(`Invalid ${paramName} code: '${langCode}'. Must match pattern: ${this.LANGUAGE_CODE_PATTERN}`);
48
+ const match = this.LANGUAGE_CODE_PATTERN.exec(trimmed);
49
+ if (!match) {
50
+ throw new Error(`Invalid ${paramName} code: '${langCode}'. Expected a BCP 47 language tag such as 'en', 'fil', 'pt-BR', 'zh-Hant' or 'es-419'`);
52
51
  }
53
- return sanitized;
52
+ const [, language, script, region] = match;
53
+ let canonical = language.toLowerCase();
54
+ if (script) {
55
+ canonical += `-${script.charAt(0).toUpperCase()}${script.slice(1).toLowerCase()}`;
56
+ }
57
+ if (region) {
58
+ canonical += `-${region.toUpperCase()}`;
59
+ }
60
+ return canonical;
54
61
  }
55
62
  /**
56
63
  * Validate array of language codes.
57
- * @param {string[]} langCodes - Array of language codes.
64
+ * @param {unknown} langCodes - Array of language codes.
58
65
  * @param {string} [paramName="languages"] - Parameter name for errors.
59
66
  * @returns {string[]} - Sanitized array of language codes.
60
67
  * @throws {Error} If array is empty or contains invalid codes.
@@ -73,7 +80,7 @@ class InputValidator {
73
80
  }
74
81
  /**
75
82
  * Validate and sanitize directory path.
76
- * @param {string} dirPath - Directory path.
83
+ * @param {unknown} dirPath - Directory path.
77
84
  * @param {string} [paramName="directory"] - Parameter name for errors.
78
85
  * @returns {string} - Resolved and validated absolute path.
79
86
  * @throws {Error} If path is invalid or outside working directory.
@@ -91,7 +98,7 @@ class InputValidator {
91
98
  }
92
99
  /**
93
100
  * Validate translation text.
94
- * @param {string} text - Text to validate.
101
+ * @param {unknown} text - Text to validate.
95
102
  * @param {string} [paramName="text"] - Parameter name for errors.
96
103
  * @returns {string} - Validated text.
97
104
  * @throws {Error} If text is invalid or too long.
@@ -110,7 +117,7 @@ class InputValidator {
110
117
  }
111
118
  /**
112
119
  * Validate translation key.
113
- * @param {string} key - Key to validate.
120
+ * @param {unknown} key - Key to validate.
114
121
  * @param {string} [paramName="key"] - Parameter name.
115
122
  * @returns {string} - Validated key.
116
123
  * @throws {Error} If key is invalid, too long, or contains traversal.
@@ -129,7 +136,7 @@ class InputValidator {
129
136
  }
130
137
  /**
131
138
  * Validate API provider name.
132
- * @param {string} provider - Provider name.
139
+ * @param {unknown} provider - Provider name.
133
140
  * @param {string} [fieldName="API provider"] - Parameter name.
134
141
  * @returns {string} - Normalized provider name.
135
142
  * @throws {Error} If provider is unknown.
@@ -157,7 +164,7 @@ class InputValidator {
157
164
  }
158
165
  let sanitized = path
159
166
  .basename(filename)
160
- .replace(/[<>:"/\\|?*\x00-\x1f]/g, "") // eslint-disable-line no-control-regex
167
+ .replace(/[<>:"/\\|?*\x00-\x1f]/g, "") // eslint-disable-line no-control-regex -- strip ASCII control characters
161
168
  .replace(/^\.+/, "")
162
169
  .trim();
163
170
  if (!sanitized) {
@@ -187,12 +194,14 @@ class InputValidator {
187
194
  }
188
195
  /**
189
196
  * Validate configuration object.
190
- * @param {any} config - Configuration object to validate.
197
+ * Rewrites `source` and `targets` in place to canonical BCP 47 casing so that
198
+ * file lookups and per-language settings all see the same tag.
199
+ * @param {unknown} config - Configuration object to validate.
191
200
  * @returns {AppConfig} - Validated configuration.
192
201
  * @throws {Error} If configuration is invalid.
193
202
  */
194
203
  static validateConfig(config) {
195
- if (!config || typeof config !== "object") {
204
+ if (!isObject(config)) {
196
205
  throw new Error("Configuration must be an object");
197
206
  }
198
207
  // Check config depth to prevent circular references
@@ -205,10 +214,10 @@ class InputValidator {
205
214
  }
206
215
  else {
207
216
  try {
208
- this.validateLanguageCode(config.source, "source language");
217
+ config.source = this.validateLanguageCode(config.source, "source language");
209
218
  }
210
219
  catch (error) {
211
- errors.push(`Invalid source language: ${error.message}`);
220
+ errors.push(`Invalid source language: ${errorMessage(error)}`);
212
221
  }
213
222
  }
214
223
  if (!config.targets || !Array.isArray(config.targets) || config.targets.length === 0) {
@@ -216,10 +225,10 @@ class InputValidator {
216
225
  }
217
226
  else {
218
227
  try {
219
- this.validateLanguageCodes(config.targets, "target languages");
228
+ config.targets = this.validateLanguageCodes(config.targets, "target languages");
220
229
  }
221
230
  catch (error) {
222
- errors.push(`Invalid target languages: ${error.message}`);
231
+ errors.push(`Invalid target languages: ${errorMessage(error)}`);
223
232
  }
224
233
  }
225
234
  if (!config.localesDir) {
@@ -231,7 +240,7 @@ class InputValidator {
231
240
  this.validateProvider(config.apiProvider, "API provider");
232
241
  }
233
242
  catch (error) {
234
- errors.push(`Invalid API provider: ${error.message}`);
243
+ errors.push(`Invalid API provider: ${errorMessage(error)}`);
235
244
  }
236
245
  }
237
246
  else {
@@ -248,27 +257,27 @@ class InputValidator {
248
257
  this.validateProvider(provider, "fallback provider");
249
258
  }
250
259
  catch (error) {
251
- errors.push(`Invalid provider in fallbackOrder: ${error.message}`);
260
+ errors.push(`Invalid provider in fallbackOrder: ${errorMessage(error)}`);
252
261
  }
253
262
  }
254
263
  }
255
264
  }
256
265
  // Concurrency validation
257
266
  if (config.concurrencyLimit !== undefined) {
258
- const limit = parseInt(config.concurrencyLimit);
267
+ const limit = parseInt(String(config.concurrencyLimit));
259
268
  if (isNaN(limit) || limit < 1 || limit > 50) {
260
269
  errors.push("'concurrencyLimit' must be a number between 1 and 50");
261
270
  }
262
271
  }
263
272
  // Cache configuration validation
264
273
  if (config.cacheSize !== undefined) {
265
- const size = parseInt(config.cacheSize);
274
+ const size = parseInt(String(config.cacheSize));
266
275
  if (isNaN(size) || size < 100 || size > 10000) {
267
276
  errors.push("'cacheSize' must be a number between 100 and 10000");
268
277
  }
269
278
  }
270
279
  if (config.cacheTTL !== undefined) {
271
- const ttl = parseInt(config.cacheTTL);
280
+ const ttl = parseInt(String(config.cacheTTL));
272
281
  if (isNaN(ttl) || ttl < 0) {
273
282
  errors.push("'cacheTTL' must be a non-negative number (milliseconds)");
274
283
  }
@@ -278,7 +287,7 @@ class InputValidator {
278
287
  }
279
288
  // API Config validation
280
289
  if (config.apiConfig) {
281
- if (typeof config.apiConfig !== "object") {
290
+ if (!isObject(config.apiConfig)) {
282
291
  errors.push("'apiConfig' must be an object");
283
292
  }
284
293
  else {
@@ -287,7 +296,7 @@ class InputValidator {
287
296
  }
288
297
  // Rate Limiter validation
289
298
  if (config.rateLimiter) {
290
- if (typeof config.rateLimiter !== "object") {
299
+ if (!isObject(config.rateLimiter)) {
291
300
  errors.push("'rateLimiter' must be an object");
292
301
  }
293
302
  else {
@@ -296,7 +305,7 @@ class InputValidator {
296
305
  }
297
306
  // Retry options validation
298
307
  if (config.retryOptions) {
299
- if (typeof config.retryOptions !== "object") {
308
+ if (!isObject(config.retryOptions)) {
300
309
  errors.push("'retryOptions' must be an object");
301
310
  }
302
311
  else {
@@ -305,7 +314,7 @@ class InputValidator {
305
314
  }
306
315
  // Context configuration validation
307
316
  if (config.context) {
308
- if (typeof config.context !== "object") {
317
+ if (!isObject(config.context)) {
309
318
  errors.push("'context' must be an object");
310
319
  }
311
320
  else {
@@ -314,7 +323,7 @@ class InputValidator {
314
323
  }
315
324
  // Quality checks validation
316
325
  if (config.qualityChecks) {
317
- if (typeof config.qualityChecks !== "object") {
326
+ if (!isObject(config.qualityChecks)) {
318
327
  errors.push("'qualityChecks' must be an object");
319
328
  }
320
329
  else {
@@ -323,7 +332,7 @@ class InputValidator {
323
332
  }
324
333
  // Length control validation
325
334
  if (config.lengthControl) {
326
- if (typeof config.lengthControl !== "object") {
335
+ if (!isObject(config.lengthControl)) {
327
336
  errors.push("'lengthControl' must be an object");
328
337
  }
329
338
  else {
@@ -332,7 +341,7 @@ class InputValidator {
332
341
  }
333
342
  // Advanced settings validation
334
343
  if (config.advanced) {
335
- if (typeof config.advanced !== "object") {
344
+ if (!isObject(config.advanced)) {
336
345
  errors.push("'advanced' must be an object");
337
346
  }
338
347
  else {
@@ -361,7 +370,7 @@ class InputValidator {
361
370
  }
362
371
  /**
363
372
  * Validate API config for each provider.
364
- * @param {Record<string, ApiProviderConfig>} apiConfig - API configuration object.
373
+ * @param {Record<string, unknown>} apiConfig - Raw API configuration object.
365
374
  * @param {string[]} errors - Array to push errors to.
366
375
  * @param {string[]} warnings - Array to push warnings to.
367
376
  */
@@ -371,13 +380,13 @@ class InputValidator {
371
380
  warnings.push(`Unknown provider in apiConfig: '${provider}'`);
372
381
  continue;
373
382
  }
374
- if (typeof settings !== "object") {
383
+ if (!isObject(settings)) {
375
384
  errors.push(`apiConfig.${provider} must be an object`);
376
385
  continue;
377
386
  }
378
387
  // Validate temperature
379
388
  if (settings.temperature !== undefined) {
380
- const temp = parseFloat(settings.temperature);
389
+ const temp = parseFloat(String(settings.temperature));
381
390
  if (isNaN(temp) || temp < 0 || temp > 2) {
382
391
  errors.push(`apiConfig.${provider}.temperature must be between 0 and 2`);
383
392
  }
@@ -387,7 +396,7 @@ class InputValidator {
387
396
  }
388
397
  // Validate maxTokens
389
398
  if (settings.maxTokens !== undefined) {
390
- const tokens = parseInt(settings.maxTokens);
399
+ const tokens = parseInt(String(settings.maxTokens));
391
400
  if (isNaN(tokens) || tokens < 100 || tokens > 8000) {
392
401
  errors.push(`apiConfig.${provider}.maxTokens must be between 100 and 8000`);
393
402
  }
@@ -396,25 +405,26 @@ class InputValidator {
396
405
  }
397
406
  /**
398
407
  * Validate rate limiter configuration.
399
- * @param {RateLimiterConfig} rateLimiter - Rate limiter configuration.
408
+ * @param {Record<string, unknown>} rateLimiter - Raw rate limiter configuration.
400
409
  * @param {string[]} errors - Array to push errors to.
401
410
  * @param {string[]} warnings - Array to push warnings to.
402
411
  */
403
412
  static validateRateLimiterConfig(rateLimiter, errors, _warnings) {
404
413
  if (rateLimiter.queueStrategy) {
405
414
  const validStrategies = ["fifo", "priority"];
406
- if (!validStrategies.includes(rateLimiter.queueStrategy)) {
415
+ if (typeof rateLimiter.queueStrategy !== "string" ||
416
+ !validStrategies.includes(rateLimiter.queueStrategy)) {
407
417
  errors.push(`rateLimiter.queueStrategy must be one of: ${validStrategies.join(", ")}`);
408
418
  }
409
419
  }
410
420
  if (rateLimiter.queueTimeout !== undefined) {
411
- const timeout = parseInt(rateLimiter.queueTimeout);
421
+ const timeout = parseInt(String(rateLimiter.queueTimeout));
412
422
  if (isNaN(timeout) || timeout < 1000 || timeout > 120000) {
413
423
  errors.push("rateLimiter.queueTimeout must be between 1000 and 120000 ms");
414
424
  }
415
425
  }
416
426
  if (rateLimiter.providerLimits) {
417
- if (typeof rateLimiter.providerLimits !== "object") {
427
+ if (!isObject(rateLimiter.providerLimits)) {
418
428
  errors.push("rateLimiter.providerLimits must be an object");
419
429
  }
420
430
  else {
@@ -423,14 +433,24 @@ class InputValidator {
423
433
  _warnings.push(`Unknown provider in providerLimits: '${provider}'`);
424
434
  continue;
425
435
  }
436
+ if (limits === null) {
437
+ errors.push(`rateLimiter.providerLimits.${provider} must be an object`);
438
+ continue;
439
+ }
440
+ // Primitives and arrays always loaded (the rate limiter ignores them), so
441
+ // they stay accepted with a warning rather than failing validation.
442
+ if (!isObject(limits) || Array.isArray(limits)) {
443
+ _warnings.push(`rateLimiter.providerLimits.${provider} should be an object, ignoring it`);
444
+ continue;
445
+ }
426
446
  if (limits.rpm !== undefined) {
427
- const rpm = parseInt(limits.rpm);
447
+ const rpm = parseInt(String(limits.rpm));
428
448
  if (isNaN(rpm) || rpm < 1 || rpm > 10000) {
429
449
  errors.push(`rateLimiter.providerLimits.${provider}.rpm must be between 1 and 10000`);
430
450
  }
431
451
  }
432
452
  if (limits.concurrency !== undefined) {
433
- const conc = parseInt(limits.concurrency);
453
+ const conc = parseInt(String(limits.concurrency));
434
454
  if (isNaN(conc) || conc < 1 || conc > 50) {
435
455
  errors.push(`rateLimiter.providerLimits.${provider}.concurrency must be between 1 and 50`);
436
456
  }
@@ -441,38 +461,40 @@ class InputValidator {
441
461
  }
442
462
  /**
443
463
  * Validate retry options.
444
- * @param {RetryOptions} retryOptions - Retry options.
464
+ * @param {Record<string, unknown>} retryOptions - Raw retry options.
445
465
  * @param {string[]} errors - Array to push errors to.
446
466
  * @param {string[]} warnings - Array to push warnings to.
447
467
  */
448
468
  static validateRetryOptions(retryOptions, errors, _warnings) {
449
469
  if (retryOptions.maxRetries !== undefined) {
450
- const retries = parseInt(retryOptions.maxRetries);
470
+ const retries = parseInt(String(retryOptions.maxRetries));
451
471
  if (isNaN(retries) || retries < 0 || retries > 10) {
452
472
  errors.push("retryOptions.maxRetries must be between 0 and 10");
453
473
  }
454
474
  }
455
475
  if (retryOptions.initialDelay !== undefined) {
456
- const delay = parseInt(retryOptions.initialDelay);
476
+ const delay = parseInt(String(retryOptions.initialDelay));
457
477
  if (isNaN(delay) || delay < 100 || delay > 10000) {
458
478
  errors.push("retryOptions.initialDelay must be between 100 and 10000 ms");
459
479
  }
460
480
  }
461
481
  if (retryOptions.maxDelay !== undefined) {
462
- const delay = parseInt(retryOptions.maxDelay);
482
+ const delay = parseInt(String(retryOptions.maxDelay));
463
483
  if (isNaN(delay) || delay < 1000 || delay > 60000) {
464
484
  errors.push("retryOptions.maxDelay must be between 1000 and 60000 ms");
465
485
  }
466
486
  }
467
- if (retryOptions.initialDelay !== undefined &&
468
- retryOptions.maxDelay !== undefined &&
469
- retryOptions.initialDelay > retryOptions.maxDelay) {
487
+ // Compare the parsed numbers like the range checks above: raw strings compare lexically.
488
+ // A missing or unparseable delay is NaN, which never compares greater.
489
+ const initialDelay = parseInt(String(retryOptions.initialDelay));
490
+ const maxDelay = parseInt(String(retryOptions.maxDelay));
491
+ if (initialDelay > maxDelay) {
470
492
  errors.push("retryOptions.initialDelay cannot be greater than maxDelay");
471
493
  }
472
494
  }
473
495
  /**
474
496
  * Validate context configuration.
475
- * @param {ContextConfig} context - Context configuration.
497
+ * @param {Record<string, unknown>} context - Raw context configuration.
476
498
  * @param {string[]} errors - Array to push errors to.
477
499
  * @param {string[]} warnings - Array to push warnings to.
478
500
  */
@@ -482,24 +504,24 @@ class InputValidator {
482
504
  this.validateProvider(context.aiProvider, "context.aiProvider");
483
505
  }
484
506
  catch (error) {
485
- errors.push(`Invalid context.aiProvider: ${error.message}`);
507
+ errors.push(`Invalid context.aiProvider: ${errorMessage(error)}`);
486
508
  }
487
509
  }
488
510
  if (context.minTextLength !== undefined) {
489
- const length = parseInt(context.minTextLength);
511
+ const length = parseInt(String(context.minTextLength));
490
512
  if (isNaN(length) || length < 0 || length > 1000) {
491
513
  errors.push("context.minTextLength must be between 0 and 1000");
492
514
  }
493
515
  }
494
- if (context.detection) {
516
+ if (isObject(context.detection)) {
495
517
  if (context.detection.threshold !== undefined) {
496
- const threshold = parseInt(context.detection.threshold);
518
+ const threshold = parseInt(String(context.detection.threshold));
497
519
  if (isNaN(threshold) || threshold < 1 || threshold > 20) {
498
520
  errors.push("context.detection.threshold must be between 1 and 20");
499
521
  }
500
522
  }
501
523
  if (context.detection.minConfidence !== undefined) {
502
- const confidence = parseFloat(context.detection.minConfidence);
524
+ const confidence = parseFloat(String(context.detection.minConfidence));
503
525
  if (isNaN(confidence) || confidence < 0 || confidence > 1) {
504
526
  errors.push("context.detection.minConfidence must be between 0 and 1");
505
527
  }
@@ -516,7 +538,7 @@ class InputValidator {
516
538
  }
517
539
  /**
518
540
  * Validate quality checks configuration.
519
- * @param {QualityChecksConfig} qualityChecks - Quality checks configuration.
541
+ * @param {Record<string, unknown>} qualityChecks - Raw quality checks configuration.
520
542
  * @param {string[]} errors - Array to push errors to.
521
543
  * @param {string[]} warnings - Array to push warnings to.
522
544
  */
@@ -530,13 +552,14 @@ class InputValidator {
530
552
  }
531
553
  /**
532
554
  * Validate length control configuration.
533
- * @param {LengthControlConfig} lengthControl - Length control configuration.
555
+ * @param {Record<string, unknown>} lengthControl - Raw length control configuration.
534
556
  * @param {string[]} errors - Array to push errors to.
535
557
  */
536
558
  static validateLengthControl(lengthControl, errors) {
537
559
  if (lengthControl.mode) {
538
560
  const validModes = ["strict", "flexible", "exact", "relaxed", "smart"];
539
- if (!validModes.includes(lengthControl.mode)) {
561
+ if (typeof lengthControl.mode !== "string" ||
562
+ !validModes.includes(lengthControl.mode)) {
540
563
  errors.push(`lengthControl.mode must be one of: ${validModes.join(", ")}`);
541
564
  }
542
565
  }
@@ -548,78 +571,32 @@ class InputValidator {
548
571
  }
549
572
  /**
550
573
  * Validate advanced configuration.
551
- * @param {AdvancedConfig} advanced - Advanced configuration.
574
+ * @param {Record<string, unknown>} advanced - Raw advanced configuration.
552
575
  * @param {string[]} errors - Array to push errors to.
553
576
  */
554
577
  static validateAdvancedConfig(advanced, errors) {
555
578
  if (advanced.timeoutMs !== undefined) {
556
- const timeout = parseInt(advanced.timeoutMs);
579
+ const timeout = parseInt(String(advanced.timeoutMs));
557
580
  if (isNaN(timeout) || timeout < 1000 || timeout > 300000) {
558
581
  errors.push("advanced.timeoutMs must be between 1000 and 300000 ms (5 min)");
559
582
  }
560
583
  }
561
584
  if (advanced.maxKeyLength !== undefined) {
562
- const length = parseInt(advanced.maxKeyLength);
585
+ const length = parseInt(String(advanced.maxKeyLength));
563
586
  if (isNaN(length) || length < 100 || length > 50000) {
564
587
  errors.push("advanced.maxKeyLength must be between 100 and 50000");
565
588
  }
566
589
  }
567
590
  if (advanced.maxBatchSize !== undefined) {
568
- const size = parseInt(advanced.maxBatchSize);
591
+ const size = parseInt(String(advanced.maxBatchSize));
569
592
  if (isNaN(size) || size < 1 || size > 100) {
570
593
  errors.push("advanced.maxBatchSize must be between 1 and 100");
571
594
  }
572
595
  }
573
596
  }
574
- /**
575
- * Sanitize translation text for security.
576
- * @param {string | null} text - Text to sanitize.
577
- * @returns {string | null} - Sanitized text.
578
- */
579
- static sanitizeTranslationText(text) {
580
- if (!text || typeof text !== "string") {
581
- return text;
582
- }
583
- let sanitized = text;
584
- for (const pattern of this.DANGEROUS_PATTERNS) {
585
- sanitized = sanitized.replace(pattern, "");
586
- }
587
- sanitized = sanitized
588
- .replace(/\r\n/g, "\n")
589
- .replace(/\r/g, "\n")
590
- .replace(/\n{3,}/g, "\n\n")
591
- .trim();
592
- return sanitized;
593
- }
594
- /**
595
- * Validate API key format.
596
- * @param {string} apiKey - API key to validate.
597
- * @param {string} providerName - Provider name.
598
- * @returns {boolean} - True if valid.
599
- * @throws {Error} If key is invalid.
600
- */
601
- static validateApiKeyFormat(apiKey, providerName) {
602
- if (!apiKey || typeof apiKey !== "string") {
603
- throw new Error(`${providerName} API key must be a non-empty string`);
604
- }
605
- if (apiKey.length < 10) {
606
- throw new Error(`${providerName} API key appears too short`);
607
- }
608
- if (apiKey.length > 200) {
609
- throw new Error(`${providerName} API key appears too long`);
610
- }
611
- const lowercaseKey = apiKey.toLowerCase();
612
- const invalidPatterns = ["test", "placeholder", "example", "your-api-key", "sk-test"];
613
- for (const pattern of invalidPatterns) {
614
- if (lowercaseKey.includes(pattern)) {
615
- throw new Error(`${providerName} API key appears to be a placeholder`);
616
- }
617
- }
618
- return true;
619
- }
620
597
  /**
621
598
  * Validate object depth to prevent circular references and deep nesting.
622
- * @param {any} obj - Object to validate.
599
+ * @param {unknown} obj - Object to validate.
623
600
  * @param {number} [maxDepth=10] - Maximum depth.
624
601
  * @param {number} [currentDepth=0] - Current depth.
625
602
  * @returns {boolean} - True if valid.
@@ -638,33 +615,5 @@ class InputValidator {
638
615
  }
639
616
  return true;
640
617
  }
641
- /**
642
- * Validate request rate for an identifier.
643
- * @param {string} identifier - Identifier (e.g., user ID or IP).
644
- * @param {number} [maxRequestsPerMinute=60] - Max requests per minute.
645
- * @returns {boolean} - True if allowed.
646
- * @throws {Error} If rate limit exceeded.
647
- */
648
- static validateRequestRate(identifier, maxRequestsPerMinute = 60) {
649
- const now = Date.now();
650
- const minute = Math.floor(now / 60000);
651
- if (!this._requestCounts) {
652
- this._requestCounts = new Map();
653
- }
654
- const key = `${identifier}-${minute}`;
655
- const currentCount = this._requestCounts.get(key) || 0;
656
- if (currentCount >= maxRequestsPerMinute) {
657
- throw new Error(`Too many requests from ${identifier}. Please try again later.`);
658
- }
659
- this._requestCounts.set(key, currentCount + 1);
660
- // Cleanup old entries
661
- for (const [k] of this._requestCounts) {
662
- const [, keyMinute] = k.split("-");
663
- if (parseInt(keyMinute) < minute - 5) {
664
- this._requestCounts.delete(k);
665
- }
666
- }
667
- return true;
668
- }
669
618
  }
670
619
  export default InputValidator;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The locale file the translations of `sourceFile` into `language` belong in, in the source's
3
+ * layout: `<root>/<source>/translation.json` gives `<root>/<language>/translation.json`, any
4
+ * other source file gives `<language>.json` next to it.
5
+ * @throws {Error} If `language` would put the file outside the locales directory.
6
+ */
7
+ export declare function targetLocaleFile(sourceFile: string, language: string): string;
@@ -0,0 +1,16 @@
1
+ import path from "path";
2
+ import InputValidator from "./input-validator.js";
3
+ /**
4
+ * The locale file the translations of `sourceFile` into `language` belong in, in the source's
5
+ * layout: `<root>/<source>/translation.json` gives `<root>/<language>/translation.json`, any
6
+ * other source file gives `<language>.json` next to it.
7
+ * @throws {Error} If `language` would put the file outside the locales directory.
8
+ */
9
+ export function targetLocaleFile(sourceFile, language) {
10
+ const sourceDir = path.dirname(sourceFile);
11
+ if (path.basename(sourceFile) === "translation.json") {
12
+ const languageDir = InputValidator.createSafeFilePath(path.dirname(sourceDir), language);
13
+ return path.join(languageDir, "translation.json");
14
+ }
15
+ return InputValidator.createSafeFilePath(sourceDir, `${language}.json`);
16
+ }
@@ -42,7 +42,7 @@ declare class Logger {
42
42
  * Parse size string (e.g., "10MB") to bytes.
43
43
  * @param sizeStr - Size string to parse.
44
44
  */
45
- parseSize(sizeStr: any): number;
45
+ parseSize(sizeStr: unknown): number;
46
46
  /**
47
47
  * Check and rotate logs if needed.
48
48
  */
@@ -64,14 +64,14 @@ declare class Logger {
64
64
  * @param message - Log message.
65
65
  * @param data - Additional data to log.
66
66
  */
67
- formatMessage(level: string, message: string, data?: any): string;
67
+ formatMessage(level: string, message: string, data?: unknown): string;
68
68
  /**
69
69
  * Write to log file.
70
70
  * @param level - Log level.
71
71
  * @param message - Log message.
72
72
  * @param data - Additional data to log.
73
73
  */
74
- writeToFile(level: string, message: string, data?: any): Promise<void>;
74
+ writeToFile(level: string, message: string, data?: unknown): Promise<void>;
75
75
  /**
76
76
  * Simple console log with debug awareness.
77
77
  * @param message - Message to log
@@ -83,32 +83,32 @@ declare class Logger {
83
83
  * @param message - Error message.
84
84
  * @param data - Additional data.
85
85
  */
86
- error(message: string, data?: any): Promise<void>;
86
+ error(message: string, data?: unknown): Promise<void>;
87
87
  /**
88
88
  * Log warning.
89
89
  * @param message - Warning message.
90
90
  * @param data - Additional data.
91
91
  */
92
- warn(message: string, data?: any): Promise<void>;
92
+ warn(message: string, data?: unknown): Promise<void>;
93
93
  /**
94
94
  * Log info.
95
95
  * @param message - Info message.
96
96
  * @param data - Additional data.
97
97
  */
98
- info(message: string, data?: any): Promise<void>;
98
+ info(message: string, data?: unknown): Promise<void>;
99
99
  /**
100
100
  * Log debug.
101
101
  * @param message - Debug message.
102
102
  * @param data - Additional data.
103
103
  */
104
- debug(message: string, data?: any): Promise<void>;
104
+ debug(message: string, data?: unknown): Promise<void>;
105
105
  /**
106
106
  * Log based on diagnostics level.
107
107
  * @param level - Log level.
108
108
  * @param message - Log message.
109
109
  * @param data - Additional data.
110
110
  */
111
- diagnostics(level: string, message: string, data?: any): Promise<void>;
111
+ diagnostics(level: string, message: string, data?: unknown): Promise<void>;
112
112
  /**
113
113
  * Get log statistics.
114
114
  * @returns Object containing log stats.