@drone1/alt 0.4.2 → 0.7.1

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 (94) hide show
  1. package/README.md +63 -62
  2. package/localization/.localization.cache.json +3100 -908
  3. package/localization/aa.json +23 -16
  4. package/localization/af.json +19 -12
  5. package/localization/agq.json +23 -16
  6. package/localization/ak.json +23 -16
  7. package/localization/am.json +23 -16
  8. package/localization/ar.json +19 -12
  9. package/localization/as.json +23 -16
  10. package/localization/asa.json +23 -16
  11. package/localization/ast.json +16 -9
  12. package/localization/az.json +17 -10
  13. package/localization/ba.json +23 -16
  14. package/localization/bas.json +23 -16
  15. package/localization/be.json +23 -16
  16. package/localization/bem.json +23 -16
  17. package/localization/bez.json +22 -15
  18. package/localization/bg.json +18 -11
  19. package/localization/bm.json +17 -10
  20. package/localization/bn.json +20 -13
  21. package/localization/bo.json +23 -16
  22. package/localization/br.json +18 -11
  23. package/localization/brx.json +23 -16
  24. package/localization/bs.json +20 -13
  25. package/localization/byn.json +23 -16
  26. package/localization/ca.json +16 -9
  27. package/localization/ccp.json +23 -16
  28. package/localization/cd-RU.json +16 -9
  29. package/localization/ceb.json +21 -14
  30. package/localization/cgg.json +22 -15
  31. package/localization/chr.json +23 -16
  32. package/localization/co.json +22 -15
  33. package/localization/config.json +1 -1
  34. package/localization/cs.json +18 -11
  35. package/localization/cu-RU.json +23 -16
  36. package/localization/da.json +14 -7
  37. package/localization/de-AT.json +19 -12
  38. package/localization/de-CH.json +19 -12
  39. package/localization/de-DE.json +18 -11
  40. package/localization/dua.json +23 -16
  41. package/localization/dv.json +23 -16
  42. package/localization/dz.json +23 -16
  43. package/localization/ebu.json +23 -16
  44. package/localization/en.json +9 -2
  45. package/localization/es-ES.json +17 -10
  46. package/localization/es-MX.json +18 -11
  47. package/localization/et.json +20 -13
  48. package/localization/eu.json +20 -13
  49. package/localization/fr-CA.json +15 -8
  50. package/localization/fr-CH.json +15 -8
  51. package/localization/fr-FR.json +15 -8
  52. package/localization/gsw.json +20 -13
  53. package/localization/hi.json +19 -12
  54. package/localization/hr.json +18 -11
  55. package/localization/hy.json +21 -14
  56. package/localization/ja.json +18 -11
  57. package/localization/km.json +21 -14
  58. package/localization/ksf.json +23 -16
  59. package/localization/ku.json +22 -15
  60. package/localization/kw.json +23 -16
  61. package/localization/my.json +22 -15
  62. package/localization/nl.json +18 -11
  63. package/localization/prs.json +18 -11
  64. package/localization/reference.js +9 -1
  65. package/localization/ru.json +14 -7
  66. package/localization/sq.json +19 -12
  67. package/localization/swc.json +21 -14
  68. package/localization/th.json +15 -8
  69. package/localization/tzm-Latn-.json +23 -16
  70. package/localization/uk.json +14 -7
  71. package/localization/vi.json +17 -10
  72. package/localization/zh-Hans.json +17 -10
  73. package/localization/zh-Hant.json +18 -11
  74. package/package.json +4 -3
  75. package/src/commands/list-models.js +6 -0
  76. package/src/{translate.js → commands/translate.js} +124 -139
  77. package/src/{consts.js → lib/consts.js} +12 -0
  78. package/src/{io.js → lib/io.js} +1 -1
  79. package/src/{logging.js → lib/logging.js} +3 -3
  80. package/src/{options.js → lib/options.js} +1 -1
  81. package/src/lib/reference-loader.js +91 -0
  82. package/src/{utils.js → lib/utils.js} +15 -0
  83. package/src/localizer/localize.js +3 -4
  84. package/src/main.mjs +98 -49
  85. package/src/providers/anthropic.mjs +38 -2
  86. package/src/providers/openai.mjs +45 -2
  87. package/src/shutdown.js +1 -1
  88. /package/{bin.mjs → alt.mjs} +0 -0
  89. /package/src/{assert.js → lib/assert.js} +0 -0
  90. /package/src/{cache.js → lib/cache.js} +0 -0
  91. /package/src/{config.js → lib/config.js} +0 -0
  92. /package/src/{context-keys.js → lib/context-keys.js} +0 -0
  93. /package/src/{logo.js → lib/logo.js} +0 -0
  94. /package/src/{provider.js → lib/provider.js} +0 -0
@@ -0,0 +1,6 @@
1
+ import { loadTranslationProvider } from '../lib/provider.js'
2
+
3
+ export async function runListModels({ appState, options, log }) {
4
+ const { apiKey, api } = await loadTranslationProvider({ __dirname: appState.__dirname, providerName: options.provider, log })
5
+ return log.I(await api.listModels(apiKey))
6
+ }
@@ -2,23 +2,22 @@ import * as path from 'path'
2
2
  import axios from 'axios'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { Listr } from 'listr2'
5
- import { localize, localizeFormatted } from './localizer/localize.js'
5
+ import { localize, localizeFormatted } from '../localizer/localize.js'
6
6
  import {
7
- DEFAULT_CACHE_FILENAME,
7
+ DEFAULT_CACHE_FILENAME, DEFAULT_LLM_MODELS,
8
8
  OVERLOADED_BACKOFF_INTERVAL_MS,
9
9
  TRANSLATION_FAILED_RESPONSE_TEXT,
10
10
  VALID_TRANSLATION_PROVIDERS
11
- } from './consts.js'
12
- import { assertValidPath } from './assert.js'
13
- import { copyFileToTempAndEnsureExtension, importJsFile, mkTmpDir, normalizeOutputPath, readFileAsText, readJsonFile, rmDir, writeJsonFile } from './io.js'
14
- import { calculateHash, normalizeData, sleep } from './utils.js'
15
- import { formatContextKeyFromKey, isContextKey } from './context-keys.js'
16
- import { loadConfig } from './config.js'
17
- import { loadTranslationProvider } from './provider.js'
18
- import { loadCache } from './cache.js'
19
- import { shutdown } from './shutdown.js'
20
-
21
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ } from '../lib/consts.js'
12
+ import { assertValidPath } from '../lib/assert.js'
13
+ import { mkTmpDir, normalizeOutputPath, readFileAsText, readJsonFile, writeJsonFile } from '../lib/io.js'
14
+ import { calculateHash, normalizeData, sleep } from '../lib/utils.js'
15
+ import { formatContextKeyFromKey, isContextKey } from '../lib/context-keys.js'
16
+ import { loadConfig } from '../lib/config.js'
17
+ import { loadTranslationProvider } from '../lib/provider.js'
18
+ import { loadCache } from '../lib/cache.js'
19
+ import { shutdown } from '../shutdown.js'
20
+ import { loadReferenceFile } from '../lib/reference-loader.js'
22
21
 
23
22
  export async function runTranslation({ appState, options, log }) {
24
23
  let exitCode = 0
@@ -70,15 +69,9 @@ export async function runTranslation({ appState, options, log }) {
70
69
  appState.tmpDir = tmpDir
71
70
 
72
71
  // Copy to a temp location first so we can ensure it has an .mjs extension
73
- const tmpReferencePath = await copyFileToTempAndEnsureExtension({
74
- filePath: options.referenceFile,
75
- tmpDir,
76
- ext: 'mjs',
77
- })
78
- const referenceContent = normalizeData(JSON.parse(JSON.stringify(await importJsFile(tmpReferencePath))), log) // TODO: Don't do this
79
- const referenceData = referenceContent[options.referenceVarName]
72
+ const referenceData = await loadReferenceFile({ appLang: appState.lang, options, tmpDir, log })
80
73
  if (!referenceData) {
81
- log.E(`No reference data found in variable "${options.referenceVarName}" in ${options.referenceFile}`)
74
+ log.E(`No reference data found in variable "${options.referenceExportedVarName}" in ${options.referenceFile}`)
82
75
  process.exit(2)
83
76
  }
84
77
 
@@ -98,7 +91,7 @@ export async function runTranslation({ appState, options, log }) {
98
91
  assertValidPath(cacheFilePath)
99
92
  appState.filesToWrite[cacheFilePath] = writableCache
100
93
 
101
- const { apiKey, api: translationProvider } = await loadTranslationProvider({ __dirname, providerName, log })
94
+ const { apiKey, api: translationProvider } = await loadTranslationProvider({ __dirname: appState.__dirname, providerName, log })
102
95
  log.V(`translation provider "${providerName}" loaded`)
103
96
 
104
97
  const addContextToTranslation = options.lookForContextData || config.lookForContextData
@@ -153,7 +146,7 @@ export async function runTranslation({ appState, options, log }) {
153
146
  suffix: contextSuffix
154
147
  })
155
148
  log.T(`contextKey=${contextKey}`)
156
- const storedHashForReferenceValue = readOnlyCache?.referenceKeyHashes?.[key]
149
+ const storedHashForReferenceValue = readOnlyCache?.referenceKeyHashes?.[targetLang]?.[key] // See https://github.com/drone1/alt/issues/1
157
150
  const storedHashForTargetLangAndValue = readOnlyCache.state[targetLang]?.keyHashes?.[key]
158
151
  const refValue = referenceData[key]
159
152
  const refContextValue = (contextKey in referenceData) ? referenceData[contextKey] : null
@@ -161,8 +154,28 @@ export async function runTranslation({ appState, options, log }) {
161
154
  const curValue = (key in outputData) ? outputData[key] : null
162
155
 
163
156
  // Skip non-string values (objects, arrays, etc.)
164
- if (typeof refValue !== 'string') {
165
- errors.push(`Value for reference key "${key}" was not a string! Skipping...`)
157
+ const refValueType = typeof refValue
158
+ if (refValueType !== 'string') {
159
+ if (refValueType === 'undefined') {
160
+ // This can happen if a user specifies a key explicitly via --keys
161
+ errors.push(
162
+ localizeFormatted({
163
+ token: 'error-value-not-in-reference-data',
164
+ data: { key },
165
+ lang: appState.lang,
166
+ log
167
+ })
168
+ )
169
+ } else {
170
+ errors.push(
171
+ localizeFormatted({
172
+ token: 'error-value-not-a-string',
173
+ data: { key, type: refValueType },
174
+ lang: appState.lang,
175
+ log
176
+ })
177
+ )
178
+ }
166
179
  continue
167
180
  }
168
181
 
@@ -249,7 +262,6 @@ export async function runTranslation({ appState, options, log }) {
249
262
  }
250
263
  }
251
264
 
252
- let nextTaskDelayMs = 0
253
265
  let totalTasks = workQueue.length
254
266
  let errorsEncountered = 0
255
267
  for (const taskInfoIdx in workQueue) {
@@ -266,18 +278,14 @@ export async function runTranslation({ appState, options, log }) {
266
278
  log
267
279
  }),
268
280
  task: async (ctx, task) => {
269
- ctx.nextTaskDelayMs = nextTaskDelayMs
270
-
271
281
  return task.newListr([
272
282
  {
273
283
  title: localize({ token: 'msg-translating', lang: appState.lang, log }),
274
284
  task: async (_, task) => {
275
285
  const translationResult = await processTranslationTask({
276
- appState, taskInfo, listrTask: task, listrCtx: ctx, options, log
286
+ appState, taskInfo, listrTask: task, options, log
277
287
  })
278
288
 
279
- nextTaskDelayMs = translationResult.nextTaskDelayMs
280
-
281
289
  if (translationResult.error) {
282
290
  ++errorsEncountered
283
291
  throw new Error(translationResult.error)
@@ -321,7 +329,7 @@ export async function runTranslation({ appState, options, log }) {
321
329
  log.I(`\x1B[38;2;44;190;78m✔\x1B[0m ${localize({ token: 'msg-nothing-to-do', lang: appState.lang, log })}`)
322
330
  }
323
331
  } catch (error) {
324
- log.E('Error:', error)
332
+ log.E(error)
325
333
  exitCode = 2
326
334
  }
327
335
 
@@ -332,50 +340,27 @@ export async function runTranslation({ appState, options, log }) {
332
340
  }
333
341
  }
334
342
 
335
- export async function processTranslationTask({ appState, taskInfo, listrTask, listrCtx, options, log }) {
343
+ export async function processTranslationTask({ appState, taskInfo, listrTask, options, log }) {
336
344
  const { key, sourceLang, targetLang, reasonsForTranslationMap, outputData, outputFilePath, writableCache, cacheFilePath, state } = taskInfo
337
- const {
338
- translationProvider,
339
- apiKey,
340
- appContextMessage,
341
- storedHashForReferenceValue,
342
- refValue,
343
- refContextValue,
344
- referenceValueHash,
345
- userMissingReferenceValueHash,
346
- userModifiedReferenceValue,
347
- curValue,
348
- currentValueHash,
349
- storedHashForTargetLangAndValue
350
- } = state
351
-
352
- let reasons = Object.keys(reasonsForTranslationMap)
345
+ const { referenceValueHash } = state
346
+
347
+ listrTask.output = Object.keys(reasonsForTranslationMap)
353
348
  .map(k => localize({ token: `msg-translation-reason-${k}`, lang: appState.lang, log }))
354
349
  .join(', ')
355
- listrTask.output = reasons
356
350
 
357
351
  const {
358
352
  success,
359
353
  translated,
360
- nextTaskDelayMs,
361
354
  newValue,
362
355
  error
363
356
  } = await translateKeyForLanguage({
364
357
  appState,
365
358
  listrTask,
366
- ctx: listrCtx,
367
- translationProvider,
368
- apiKey,
369
- referenceValueHash,
370
- storedHashForReferenceValue,
371
- storedHashForTargetLangAndValue,
372
359
  sourceLang,
373
360
  targetLang,
374
361
  key,
375
- refValue,
376
- refContextValue,
377
- curValue,
378
362
  options,
363
+ state,
379
364
  log
380
365
  })
381
366
 
@@ -398,7 +383,8 @@ export async function processTranslationTask({ appState, taskInfo, listrTask, li
398
383
  listrTask.output = localizeFormatted({ token: 'msg-show-translation-result', data: { key, newValue }, lang: appState.lang, log })
399
384
 
400
385
  // Update the hash for the reference key, so we can monitor if the user changed a specific key
401
- writableCache.referenceKeyHashes[key] = referenceValueHash
386
+ writableCache.referenceKeyHashes[targetLang] = writableCache.referenceKeyHashes[targetLang] || {}
387
+ writableCache.referenceKeyHashes[targetLang][key] = referenceValueHash
402
388
 
403
389
  // Update state file every time, in case the user kills the process
404
390
  if (options.realtimeWrites) {
@@ -420,57 +406,43 @@ export async function processTranslationTask({ appState, taskInfo, listrTask, li
420
406
  appState.filesToWrite[outputFilePath] = outputData
421
407
  }
422
408
 
423
- return { nextTaskDelayMs, error }
409
+ return { error }
424
410
  }
425
411
 
426
412
  async function translateKeyForLanguage({
427
413
  appState,
428
414
  listrTask,
429
- ctx,
430
- translationProvider,
431
- apiKey,
432
- appContextMessage,
433
415
  sourceLang,
434
416
  targetLang,
417
+ state,
435
418
  key,
436
- refValue,
437
- refContextValue,
438
- options: { maxRetries },
419
+ options: { maxRetries, model },
439
420
  log
440
421
  }) {
441
- const result = { success: false, translated: false, newValue: null, nextTaskDelayMs: 0, error: null }
422
+ const { translationProvider, apiKey, appContextMessage, refValue, refContextValue } = state
423
+ const result = { success: false, translated: false, newValue: null, error: null }
424
+
425
+ const providerName = translationProvider.name().toLowerCase()
426
+ model = model ?? DEFAULT_LLM_MODELS[providerName]
427
+ if (!model?.length) {
428
+ throw new Error(
429
+ localizeFormatted({ token: 'error-invalid-llm-model', data: { model }, lang: appState.lang, log })
430
+ )
431
+ }
442
432
 
443
433
  // Call translation provider
444
434
  log.D(`[${targetLang}] Translating "${key}"...`)
445
435
  listrTask.output = localizeFormatted({ token: 'msg-translating-key', data: { key }, lang: appState.lang, log })
446
436
 
447
- const providerName = translationProvider.name()
448
- let translated = null
449
437
  let newValue
450
438
 
451
- // Because of the simple (naive) way we handle being rate-limited and backing off, we kind of want to retry forever but not forever.
452
- // A single key may need to retry many times, since the algorithm is quite simple: if a task is told to retry after 10s,
453
- // any subsequent tasks that run will delay 10s also, then those concurrent remaining tasks will all hammer at once, some
454
- // will complete (maybe), then we'll wait again, then hammer again. A more proper solution may or may not be forthcoming...
455
- for (let attempt = 0; !newValue && attempt <= maxRetries; ++attempt) {
439
+ for (let attempt = 0; !newValue?.length && attempt <= maxRetries; ++attempt) {
456
440
  const attemptStr = attempt > 0 ? ` [Attempt: ${attempt + 1}]` : ''
457
441
  log.D(`[translate] attempt=${attempt}`)
458
442
 
459
- log.D('next task delay', ctx.nextTaskDelayMs)
460
- if (ctx.nextTaskDelayMs > 0) {
461
- const msg = localizeFormatted({
462
- token: 'msg-rate-limited-sleeping',
463
- data: { interval: Math.floor(ctx.nextTaskDelayMs / 1000), attemptStr }, lang: appState.lang, log
464
- })
465
- listrTask.output = msg
466
- log.D(msg)
467
- await sleep(ctx.nextTaskDelayMs)
468
- }
469
-
470
443
  const translateResult = await translate({
471
444
  appState,
472
445
  listrTask,
473
- ctx,
474
446
  provider: translationProvider,
475
447
  text: refValue,
476
448
  context: refContextValue,
@@ -478,16 +450,23 @@ async function translateKeyForLanguage({
478
450
  targetLang,
479
451
  appContextMessage,
480
452
  apiKey,
453
+ model,
481
454
  maxRetries: maxRetries,
482
455
  attemptStr,
483
456
  log
484
457
  })
485
458
 
486
- if (translateResult.backoffInterval > 0) {
487
- log.D(`backing off... interval: ${translateResult.backoffInterval}`)
488
- //listrTask.output = 'Rate limited'
489
- log.D(`ctx.nextTaskDelayMs=${ctx.nextTaskDelayMs}`)
490
- result.nextTaskDelayMs = Math.max(ctx.nextTaskDelayMs, translateResult.backoffInterval)
459
+ const { backoffInterval } = translateResult
460
+ if (backoffInterval > 0) {
461
+ log.D(`backing off... interval: ${backoffInterval}`)
462
+
463
+ if (backoffInterval > 0) {
464
+ listrTask.output = localizeFormatted({
465
+ token: 'msg-rate-limited-sleeping',
466
+ data: { interval: Math.floor(backoffInterval / 1000), attemptStr }, lang: appState.lang, log
467
+ })
468
+ await sleep(backoffInterval)
469
+ }
491
470
  } else {
492
471
  newValue = translateResult.translated
493
472
  result.success = true
@@ -499,9 +478,40 @@ async function translateKeyForLanguage({
499
478
  result.translated = true
500
479
  result.newValue = newValue
501
480
  } else {
502
- result.error = `Translation was empty; target language=${targetLang}; key=${key}; text=${refValue}`
481
+ result.error = localizeFormatted({ token: 'error-translation-failed', data: { targetLang, key, refValue }, lang: appState.lang, log })
482
+ }
483
+
484
+ return result
485
+ }
486
+
487
+ async function translate({
488
+ appState,
489
+ listrTask,
490
+ provider,
491
+ appContextMessage,
492
+ text,
493
+ context,
494
+ sourceLang,
495
+ targetLang,
496
+ apiKey,
497
+ model,
498
+ attemptStr,
499
+ log
500
+ }) {
501
+ log.D(`[translate] sourceLang=${sourceLang}; targetLang=${targetLang}; text=${text}`)
502
+ const result = { translated: null, backoffInterval: 0 }
503
+
504
+ if (sourceLang === targetLang) {
505
+ log.D(`Using reference value since source & target language are the same`)
506
+ result.translated = text
507
+ } else {
508
+ await translateTextViaProvider({
509
+ appState, provider, listrTask, sourceLang, targetLang, appContextMessage, context, text, log, apiKey, model, attemptStr, providerName: provider.name(), outResult: result
510
+ })
503
511
  }
504
512
 
513
+ log.D(`[translate] `, result)
514
+
505
515
  return result
506
516
  }
507
517
 
@@ -516,9 +526,10 @@ async function translateTextViaProvider({
516
526
  text,
517
527
  log,
518
528
  apiKey,
529
+ model,
519
530
  attemptStr,
520
- result,
521
- providerName
531
+ providerName,
532
+ outResult
522
533
  }) {
523
534
  try {
524
535
  const providerName = provider.name()
@@ -541,7 +552,7 @@ async function translateTextViaProvider({
541
552
  + `\n\n${text}`
542
553
  )
543
554
  log.D(`prompt: `, messages)
544
- const { url, params, config } = provider.getTranslationRequestDetails({ messages, apiKey, log })
555
+ const { url, params, config } = provider.getTranslationRequestDetails({ model, messages, apiKey, log })
545
556
  log.T('url: ', url, 'params: ', params, 'config: ', config)
546
557
  listrTask.output = localizeFormatted({ token: 'msg-hitting-provider-endpoint', data: { providerName, attemptStr }, lang: appState.lang, log })
547
558
  const response = await axios.post(url, params, config)
@@ -550,53 +561,27 @@ async function translateTextViaProvider({
550
561
  if (!translated?.length) throw new Error(`${providerName} translated text to empty string. You may need to top up your credits.`)
551
562
  log.D(`${translated}`)
552
563
  if (translated === TRANSLATION_FAILED_RESPONSE_TEXT) throw new Error(`${providerName} failed to translate string to ${targetLang}; string: ${text}`)
553
- result.translated = translated
564
+ outResult.translated = translated
554
565
  } catch (error) {
566
+ let errorHandled = false
567
+
555
568
  const response = error?.response
556
569
  if (response) {
557
570
  if (response.status === 429) {
558
- result.backoffInterval = provider.getSleepInterval(response.headers, log)
559
- log.D(`Rate limited; retrying in ${result.backoffInterval}`)
560
- } else if (error.response.status === 529) { // Unofficial 'overloaded' code
561
- result.backoffInterval = OVERLOADED_BACKOFF_INTERVAL_MS
562
- log.D(`Overloaded; retrying in ${result.backoffInterval}`)
563
- listrTask.output = `${providerName} overloaded; retrying in ${result.backoffInterval / 1000}s `
571
+ outResult.backoffInterval = provider.getSleepInterval(response.headers, log)
572
+ log.D(`Rate limited; retrying in ${outResult.backoffInterval}`)
573
+ errorHandled = true
574
+ } else if (response.status === 529) { // Unofficial 'overloaded' code
575
+ outResult.backoffInterval = OVERLOADED_BACKOFF_INTERVAL_MS
576
+ log.D(`Overloaded; retrying in ${outResult.backoffInterval}`)
577
+ listrTask.output = `${providerName} overloaded; retrying in ${outResult.backoffInterval / 1000}s `
578
+ errorHandled = true
564
579
  }
565
- } else {
566
- log.W(`API failed. Error:`, error.message)
567
580
  }
568
- }
569
- }
570
-
571
- async function translate({
572
- appState,
573
- listrTask,
574
- ctx,
575
- provider,
576
- appContextMessage,
577
- text,
578
- context,
579
- sourceLang,
580
- targetLang,
581
- apiKey,
582
- attemptStr,
583
- log
584
- }) {
585
- log.D(`[translate] sourceLang=${sourceLang}; targetLang=${targetLang}; text=${text}`)
586
- const result = { translated: null, backoffInterval: 0 }
587
-
588
- const providerName = provider.name()
589
581
 
590
- if (sourceLang === targetLang) {
591
- log.D(`Using reference value since source & target language are the same`)
592
- result.translated = text
593
- } else {
594
- await translateTextViaProvider({
595
- appState, provider, listrTask, sourceLang, targetLang, appContextMessage, context, text, log, apiKey, attemptStr, result, providerName
596
- })
582
+ if (!errorHandled) {
583
+ log.W(`${providerName} API failed. Error:`, error?.message ?? error)
584
+ }
597
585
  }
598
-
599
- log.D(`[translate] `, result)
600
-
601
- return result
602
586
  }
587
+
@@ -21,3 +21,15 @@ export const DEFAULT_CONFIG_FILENAME = 'config.json'
21
21
  export const OVERLOADED_BACKOFF_INTERVAL_MS = 30 * 1000
22
22
  export const CWD = process.cwd()
23
23
 
24
+ export const SUPPORTED_REFERENCE_FILE_EXTENSIONS = [
25
+ 'js',
26
+ 'mjs',
27
+ 'json',
28
+ 'jsonc'
29
+ ]
30
+
31
+ export const DEFAULT_LLM_MODELS = {
32
+ anthropic: 'claude-3-7-sonnet-20250219',
33
+ openai: 'gpt-4-turbo'
34
+ }
35
+
@@ -33,7 +33,7 @@ export async function readFileAsText(filePath) {
33
33
 
34
34
  export async function readJsonFile(filePath, isJSONComments = false) {
35
35
  let content = await readFileAsText(filePath)
36
- if (isJSONComments) content = stripJsonComments.stripJsonComments(content)
36
+ if (isJSONComments) content = stripJsonComments(content)
37
37
  return parseJson(content)
38
38
  }
39
39
 
@@ -1,12 +1,12 @@
1
- export function createLog() {
1
+ export function createLog(isDevMode) {
2
2
  return {
3
3
  // T/D/V blackholed until program options are parsed
4
4
  T: () => { },
5
5
  D: () => { },
6
6
  V: () => { },
7
7
 
8
- E: function(...args) {
9
- console.error(...args)
8
+ E: function(args) {
9
+ console.error(isDevMode ? args : (args?.message ?? args))
10
10
  },
11
11
  W: function(...args) {
12
12
  console.warn(...args)
@@ -1,4 +1,4 @@
1
- import { isBcp47LanguageTagValid } from './localizer/localize.js'
1
+ import { isBcp47LanguageTagValid } from '../localizer/localize.js'
2
2
  import { unique } from './utils.js'
3
3
 
4
4
  // Helper function to parse comma-separated list
@@ -0,0 +1,91 @@
1
+ import { copyFileToTempAndEnsureExtension, importJsFile, readJsonFile } from './io.js'
2
+ import { getFileExtension } from './utils.js'
3
+ import { SUPPORTED_REFERENCE_FILE_EXTENSIONS } from './consts.js'
4
+ import { localizeFormatted } from '../localizer/localize.js'
5
+
6
+ export async function loadReferenceFile({ appLang, options: { referenceFile, referenceExportedVarName }, tmpDir, log }) {
7
+ const ext = getFileExtension(referenceFile)?.toLowerCase()
8
+ if (!SUPPORTED_REFERENCE_FILE_EXTENSIONS.includes(ext)) {
9
+ throw new Error(
10
+ localizeFormatted({
11
+ token: 'error-bad-reference-file-ext',
12
+ data: { ext },
13
+ lang: appLang,
14
+ log
15
+ })
16
+ )
17
+ }
18
+
19
+ let content
20
+ let useRefVar
21
+ switch(ext) {
22
+ case 'js': {
23
+ log.D(`Reading JS file "${referenceFile}"...`)
24
+
25
+ // For .js, we need to copy to a temp location as an .mjs so we can dynamically import
26
+ const tmpReferencePath = await copyFileToTempAndEnsureExtension({
27
+ filePath: referenceFile,
28
+ tmpDir,
29
+ ext: 'mjs',
30
+ })
31
+ //const referenceContent = normalizeData(await importJsFile(tmpReferencePath), log)
32
+ content = await importJsFile(tmpReferencePath)
33
+ useRefVar = true
34
+ break
35
+ }
36
+
37
+ case 'mjs': {
38
+ // We can dynamically import an .mjs directly from its actual path
39
+ log.D(`Reading MJS file "${referenceFile}"...`)
40
+ content = await importJsFile(referenceFile)
41
+ useRefVar = true
42
+ break
43
+ }
44
+
45
+ case 'json': {
46
+ log.D(`Reading JSON file "${referenceFile}"...`)
47
+ content = await readJsonFile(referenceFile, false)
48
+ useRefVar = false
49
+ break
50
+ }
51
+
52
+ case 'jsonc': {
53
+ log.D(`Reading JSONC file "${referenceFile}"...`)
54
+ content = await readJsonFile(referenceFile, true)
55
+ useRefVar = false
56
+ break
57
+ }
58
+ }
59
+
60
+ if (!content) {
61
+ throw new Error(
62
+ localizeFormatted({
63
+ token: 'error-reference-file-load-failed',
64
+ data: { referenceFile },
65
+ lang: appLang,
66
+ log
67
+ })
68
+ )
69
+ }
70
+
71
+ let result
72
+ if (useRefVar && referenceExportedVarName?.length) {
73
+ log.D(`[loadReferenceFile] useRefVar: ${useRefVar}`)
74
+ if (!(referenceExportedVarName in content)) {
75
+ throw new Error(
76
+ localizeFormatted({
77
+ token: 'error-reference-var-not-found-in-data',
78
+ data: { referenceExportedVarName, referenceFile, possibleKeys: Object.keys(content) },
79
+ lang: appLang,
80
+ log
81
+ })
82
+ )
83
+ }
84
+
85
+ result = content[referenceExportedVarName]
86
+ } else {
87
+ result = content
88
+ }
89
+
90
+ return result
91
+ }
@@ -106,3 +106,18 @@ export function normalizeData(data) {
106
106
  }
107
107
  return normalizedData
108
108
  }
109
+
110
+ export function pick(o, ...props) {
111
+ return Object.assign(
112
+ {},
113
+ ...props.filter(prop => o[prop] !== undefined)
114
+ .map(prop => ({ [prop]: o[prop] }))
115
+ )
116
+ }
117
+
118
+ // Returns the extension without the '.'
119
+ export function getFileExtension(path) {
120
+ if (path.indexOf('.') < 0) return null
121
+ return path.split('.').pop()
122
+ }
123
+
@@ -1,9 +1,8 @@
1
- import * as fsp from 'fs/promises'
2
1
  import * as locale from 'locale-codes'
3
2
  import * as path from 'path'
4
- import { obj2Str, replaceStringVarsWithObjectValues } from '../utils.js'
5
- import { fileExists, readJsonFile } from '../io.js'
6
- import { LANGTAG_DEFAULT } from '../consts.js'
3
+ import { obj2Str, replaceStringVarsWithObjectValues } from '../lib/utils.js'
4
+ import { readJsonFile } from '../lib/io.js'
5
+ import { LANGTAG_DEFAULT } from '../lib/consts.js'
7
6
 
8
7
  const LocalizationMap = {}
9
8