@link-assistant/hive-mind 1.69.11 → 1.69.13

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.
@@ -22,6 +22,7 @@ export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
23
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
24
24
  import { t } from './i18n.lib.mjs';
25
+ import { lt } from './limits-i18n.lib.mjs';
25
26
 
26
27
  export const QueueItemStatus = {
27
28
  QUEUED: 'queued',
@@ -32,6 +33,23 @@ export const QueueItemStatus = {
32
33
  CANCELLED: 'cancelled',
33
34
  };
34
35
 
36
+ function getLocale(options = {}) {
37
+ if (typeof options === 'string') return options;
38
+ return options?.locale || null;
39
+ }
40
+
41
+ function appendWaitingForCurrentCommand(reason, locale) {
42
+ return `${reason} (${lt('queue_waiting_current_command', {}, { locale })})`;
43
+ }
44
+
45
+ function appendRemainingDuration(reason, ms, locale) {
46
+ return `${reason} (${lt('remaining', { duration: formatDuration(ms, { locale }) }, { locale })})`;
47
+ }
48
+
49
+ function queueStatusLabel(status, locale) {
50
+ return lt(`queue_status_${status}`, {}, { locale });
51
+ }
52
+
35
53
  /**
36
54
  * Queue item representing a /solve command request
37
55
  */
@@ -373,7 +391,7 @@ export class SolveQueue {
373
391
  if (toolQueue.length === 0) continue;
374
392
 
375
393
  // Check if first item in this tool's queue can start
376
- const check = await this.canStartCommand({ tool });
394
+ const check = await this.canStartCommand({ tool, locale: toolQueue[0]?.locale || null });
377
395
 
378
396
  // When a 'reject' strategy threshold is exceeded, immediately reject
379
397
  // all items in this tool's queue instead of leaving them waiting.
@@ -411,16 +429,16 @@ export class SolveQueue {
411
429
  * @see https://github.com/link-assistant/hive-mind/issues/1555
412
430
  */
413
431
  async rejectAllItemsInQueue(tool, toolQueue, rejectReason) {
414
- const reason = rejectReason || 'Resource limit exceeded';
415
432
  while (toolQueue.length > 0) {
416
433
  const item = toolQueue.shift();
434
+ const reason = rejectReason || lt('resource_limit_exceeded', {}, { locale: item.locale });
417
435
  item.setFailed(reason);
418
436
  this.failed.push(item);
419
437
  this.stats.totalFailed++;
420
438
 
421
439
  this.log(`Rejected queued item: ${item.toString()} from ${tool} queue - ${reason}`);
422
440
 
423
- await this.updateItemMessage(item, `āŒ Solve command rejected.\n\n${item.infoBlock}\n\n🚫 Reason: ${reason}`);
441
+ await this.updateItemMessage(item, t('telegram.solve_rejected', { infoBlock: item.infoBlock, reason }, { locale: item.locale }));
424
442
  }
425
443
  while (this.failed.length > 100) this.failed.shift();
426
444
  }
@@ -546,6 +564,7 @@ export class SolveQueue {
546
564
  */
547
565
  async canStartCommand(options = {}) {
548
566
  const tool = options.tool || 'claude';
567
+ const locale = getLocale(options);
549
568
  const reasons = [];
550
569
  let oneAtATime = false;
551
570
  let rejected = false;
@@ -558,8 +577,8 @@ export class SolveQueue {
558
577
  if (lastStartTime) {
559
578
  const timeSinceLastStart = Date.now() - lastStartTime;
560
579
  if (timeSinceLastStart < QUEUE_CONFIG.MIN_START_INTERVAL_MS) {
561
- const waitSeconds = Math.ceil((QUEUE_CONFIG.MIN_START_INTERVAL_MS - timeSinceLastStart) / 1000);
562
- reasons.push(formatWaitingReason('min_interval', 0, 0) + ` (${waitSeconds}s remaining)`);
580
+ const waitMs = QUEUE_CONFIG.MIN_START_INTERVAL_MS - timeSinceLastStart;
581
+ reasons.push(appendRemainingDuration(formatWaitingReason('min_interval', 0, 0, { locale }), waitMs, locale));
563
582
  this.recordThrottle('min_interval');
564
583
  }
565
584
  }
@@ -609,7 +628,7 @@ export class SolveQueue {
609
628
  // System resources apply to ALL tools, not just Claude
610
629
  // See: https://github.com/link-assistant/hive-mind/issues/1155
611
630
  // See: https://github.com/link-assistant/hive-mind/issues/1253 (strategies)
612
- const resourceCheck = await this.checkSystemResources(totalProcessing);
631
+ const resourceCheck = await this.checkSystemResources(totalProcessing, { locale });
613
632
  if (resourceCheck.rejected) {
614
633
  rejected = true;
615
634
  rejectReason = resourceCheck.rejectReason;
@@ -628,7 +647,7 @@ export class SolveQueue {
628
647
  // See: https://github.com/link-assistant/hive-mind/issues/1253 (strategies)
629
648
  const hasRunningToolProcess = (externalProcessing.byTool[tool] || 0) > 0;
630
649
  const toolProcessingCount = this.getProcessingCountByTool(tool);
631
- const limitCheck = await this.checkApiLimits(hasRunningToolProcess, toolProcessingCount, tool);
650
+ const limitCheck = await this.checkApiLimits(hasRunningToolProcess, toolProcessingCount, tool, { locale });
632
651
  if (limitCheck.rejected) {
633
652
  rejected = true;
634
653
  rejectReason = limitCheck.rejectReason;
@@ -646,16 +665,16 @@ export class SolveQueue {
646
665
  // Add claude_running info at the END (not beginning) of reasons
647
666
  // Since it's supplementary info, not the primary blocking reason
648
667
  // See: https://github.com/link-assistant/hive-mind/issues/1078
649
- reasons.push(formatWaitingReason('claude_running', claudeProcessCount, 0) + ` (${claudeProcessCount} processes)`);
668
+ reasons.push(`${formatWaitingReason('claude_running', claudeProcessCount, 0, { locale })} (${lt('queue_processes', { count: claudeProcessCount }, { locale })})`);
650
669
  }
651
670
  if (tool === 'codex' && hasRunningCodex && reasons.length > 0) {
652
- reasons.push(formatWaitingReason('codex_running', codexProcessCount, 0) + ` (${codexProcessCount} processes)`);
671
+ reasons.push(`${formatWaitingReason('codex_running', codexProcessCount, 0, { locale })} (${lt('queue_processes', { count: codexProcessCount }, { locale })})`);
653
672
  }
654
673
  if (tool === 'qwen' && hasRunningQwen && reasons.length > 0) {
655
- reasons.push(formatWaitingReason('qwen_running', qwenProcessCount, 0) + ` (${qwenProcessCount} processes)`);
674
+ reasons.push(`${formatWaitingReason('qwen_running', qwenProcessCount, 0, { locale })} (${lt('queue_processes', { count: qwenProcessCount }, { locale })})`);
656
675
  }
657
676
  if (tool === 'gemini' && hasRunningGemini && reasons.length > 0) {
658
- reasons.push(formatWaitingReason('gemini_running', geminiProcessCount, 0) + ` (${geminiProcessCount} processes)`);
677
+ reasons.push(`${formatWaitingReason('gemini_running', geminiProcessCount, 0, { locale })} (${lt('queue_processes', { count: geminiProcessCount }, { locale })})`);
659
678
  }
660
679
 
661
680
  const canStart = reasons.length === 0 && !rejected;
@@ -712,7 +731,8 @@ export class SolveQueue {
712
731
  * @param {number} totalProcessing - Total processing count (queue + external claude processes)
713
732
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
714
733
  */
715
- async checkSystemResources(totalProcessing = 0) {
734
+ async checkSystemResources(totalProcessing = 0, options = {}) {
735
+ const locale = getLocale(options);
716
736
  const reasons = [];
717
737
  let oneAtATime = false;
718
738
  let rejected = false;
@@ -723,7 +743,7 @@ export class SolveQueue {
723
743
  if (memResult.success) {
724
744
  const usedRatio = memResult.memory.usedPercentage / 100;
725
745
  if (usedRatio >= QUEUE_CONFIG.thresholds.ram.value) {
726
- const reason = formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.thresholds.ram.value);
746
+ const reason = formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.thresholds.ram.value, { locale });
727
747
  const strategy = QUEUE_CONFIG.thresholds.ram.strategy;
728
748
  this.recordThrottle(`ram_${strategy}`);
729
749
 
@@ -733,7 +753,7 @@ export class SolveQueue {
733
753
  } else if (strategy === 'dequeue-one-at-a-time') {
734
754
  oneAtATime = true;
735
755
  if (totalProcessing > 0) {
736
- reasons.push(reason + ' (waiting for current command)');
756
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
737
757
  }
738
758
  } else {
739
759
  // 'enqueue' - block unconditionally
@@ -760,7 +780,7 @@ export class SolveQueue {
760
780
  }
761
781
 
762
782
  if (usageRatio >= QUEUE_CONFIG.thresholds.cpu.value) {
763
- const reason = formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.thresholds.cpu.value);
783
+ const reason = formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.thresholds.cpu.value, { locale });
764
784
  const strategy = QUEUE_CONFIG.thresholds.cpu.strategy;
765
785
  this.recordThrottle(`cpu_${strategy}`);
766
786
 
@@ -770,7 +790,7 @@ export class SolveQueue {
770
790
  } else if (strategy === 'dequeue-one-at-a-time') {
771
791
  oneAtATime = true;
772
792
  if (totalProcessing > 0) {
773
- reasons.push(reason + ' (waiting for current command)');
793
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
774
794
  }
775
795
  } else {
776
796
  // 'enqueue' - block unconditionally
@@ -788,7 +808,7 @@ export class SolveQueue {
788
808
  const usedPercent = 100 - diskResult.diskSpace.freePercentage;
789
809
  const usedRatio = usedPercent / 100;
790
810
  if (usedRatio >= QUEUE_CONFIG.thresholds.disk.value) {
791
- const reason = formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.thresholds.disk.value);
811
+ const reason = formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.thresholds.disk.value, { locale });
792
812
  const strategy = QUEUE_CONFIG.thresholds.disk.strategy;
793
813
  this.recordThrottle(`disk_${strategy}`);
794
814
 
@@ -798,7 +818,7 @@ export class SolveQueue {
798
818
  } else if (strategy === 'dequeue-one-at-a-time') {
799
819
  oneAtATime = true;
800
820
  if (totalProcessing > 0) {
801
- reasons.push(reason + ' (waiting for current command)');
821
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
802
822
  }
803
823
  } else {
804
824
  // 'enqueue' - block unconditionally
@@ -833,7 +853,8 @@ export class SolveQueue {
833
853
  * @param {string} tool - The tool being used ('claude', 'agent', 'codex', 'gemini', 'qwen', etc.)
834
854
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
835
855
  */
836
- async checkApiLimits(hasRunningToolProcess = false, toolProcessingCount = 0, tool = 'claude') {
856
+ async checkApiLimits(hasRunningToolProcess = false, toolProcessingCount = 0, tool = 'claude', options = {}) {
857
+ const locale = getLocale(options);
837
858
  const reasons = [];
838
859
  let oneAtATime = false;
839
860
  let rejected = false;
@@ -862,7 +883,7 @@ export class SolveQueue {
862
883
  if (sessionPercent !== null) {
863
884
  const sessionRatio = sessionPercent / 100;
864
885
  if (sessionRatio >= QUEUE_CONFIG.thresholds.claude5Hour.value) {
865
- const reason = formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.claude5Hour.value);
886
+ const reason = formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.claude5Hour.value, { locale });
866
887
  const strategy = QUEUE_CONFIG.thresholds.claude5Hour.strategy;
867
888
  this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : `claude_5_hour_session_${strategy}`);
868
889
 
@@ -872,7 +893,7 @@ export class SolveQueue {
872
893
  } else if (strategy === 'dequeue-one-at-a-time') {
873
894
  oneAtATime = true;
874
895
  if (totalToolProcessing > 0) {
875
- reasons.push(reason + ' (waiting for current command)');
896
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
876
897
  }
877
898
  } else {
878
899
  // 'enqueue' - block unconditionally
@@ -887,7 +908,7 @@ export class SolveQueue {
887
908
  if (weeklyPercent !== null) {
888
909
  const weeklyRatio = weeklyPercent / 100;
889
910
  if (weeklyRatio >= QUEUE_CONFIG.thresholds.claudeWeekly.value) {
890
- const reason = formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.claudeWeekly.value);
911
+ const reason = formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.claudeWeekly.value, { locale });
891
912
  const strategy = QUEUE_CONFIG.thresholds.claudeWeekly.strategy;
892
913
  this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : `claude_weekly_${strategy}`);
893
914
 
@@ -897,7 +918,7 @@ export class SolveQueue {
897
918
  } else if (strategy === 'dequeue-one-at-a-time') {
898
919
  oneAtATime = true;
899
920
  if (totalToolProcessing > 0) {
900
- reasons.push(reason + ' (waiting for current command)');
921
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
901
922
  }
902
923
  } else {
903
924
  // 'enqueue' - block unconditionally
@@ -915,7 +936,7 @@ export class SolveQueue {
915
936
  if (sessionPercent !== null) {
916
937
  const sessionRatio = sessionPercent / 100;
917
938
  if (sessionRatio >= QUEUE_CONFIG.thresholds.codex5Hour.value) {
918
- const reason = formatWaitingReason('codex_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.codex5Hour.value);
939
+ const reason = formatWaitingReason('codex_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.codex5Hour.value, { locale });
919
940
  const strategy = QUEUE_CONFIG.thresholds.codex5Hour.strategy;
920
941
  this.recordThrottle(sessionRatio >= 1.0 ? 'codex_5_hour_session_100' : `codex_5_hour_session_${strategy}`);
921
942
 
@@ -925,7 +946,7 @@ export class SolveQueue {
925
946
  } else if (strategy === 'dequeue-one-at-a-time') {
926
947
  oneAtATime = true;
927
948
  if (totalToolProcessing > 0) {
928
- reasons.push(reason + ' (waiting for current command)');
949
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
929
950
  }
930
951
  } else {
931
952
  reasons.push(reason);
@@ -936,7 +957,7 @@ export class SolveQueue {
936
957
  if (weeklyPercent !== null) {
937
958
  const weeklyRatio = weeklyPercent / 100;
938
959
  if (weeklyRatio >= QUEUE_CONFIG.thresholds.codexWeekly.value) {
939
- const reason = formatWaitingReason('codex_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.codexWeekly.value);
960
+ const reason = formatWaitingReason('codex_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.codexWeekly.value, { locale });
940
961
  const strategy = QUEUE_CONFIG.thresholds.codexWeekly.strategy;
941
962
  this.recordThrottle(weeklyRatio >= 1.0 ? 'codex_weekly_100' : `codex_weekly_${strategy}`);
942
963
 
@@ -946,7 +967,7 @@ export class SolveQueue {
946
967
  } else if (strategy === 'dequeue-one-at-a-time') {
947
968
  oneAtATime = true;
948
969
  if (totalToolProcessing > 0) {
949
- reasons.push(reason + ' (waiting for current command)');
970
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
950
971
  }
951
972
  } else {
952
973
  reasons.push(reason);
@@ -967,7 +988,7 @@ export class SolveQueue {
967
988
  const usedPercent = githubResult.githubRateLimit.usedPercentage;
968
989
  const usedRatio = usedPercent / 100;
969
990
  if (usedRatio >= QUEUE_CONFIG.thresholds.githubApi.value) {
970
- const reason = formatWaitingReason('github', usedPercent, QUEUE_CONFIG.thresholds.githubApi.value);
991
+ const reason = formatWaitingReason('github', usedPercent, QUEUE_CONFIG.thresholds.githubApi.value, { locale });
971
992
  const strategy = QUEUE_CONFIG.thresholds.githubApi.strategy;
972
993
  this.recordThrottle(usedRatio >= 1.0 ? 'github_100' : `github_${strategy}`);
973
994
 
@@ -977,7 +998,7 @@ export class SolveQueue {
977
998
  } else if (strategy === 'dequeue-one-at-a-time') {
978
999
  oneAtATime = true;
979
1000
  if (totalToolProcessing > 0) {
980
- reasons.push(reason + ' (waiting for current command)');
1001
+ reasons.push(appendWaitingForCurrentCommand(reason, locale));
981
1002
  }
982
1003
  } else {
983
1004
  // 'enqueue' - block unconditionally
@@ -1125,7 +1146,7 @@ export class SolveQueue {
1125
1146
  // First check if the tool's threshold triggers a 'reject' strategy.
1126
1147
  // If so, reject all items at once rather than iterating one by one.
1127
1148
  // See: https://github.com/link-assistant/hive-mind/issues/1555
1128
- const toolCheck = await this.canStartCommand({ tool });
1149
+ const toolCheck = await this.canStartCommand({ tool, locale: toolQueue[0]?.locale || null });
1129
1150
  if (toolCheck.rejected) {
1130
1151
  await this.rejectAllItemsInQueue(tool, toolQueue, toolCheck.rejectReason);
1131
1152
  continue;
@@ -1134,9 +1155,10 @@ export class SolveQueue {
1134
1155
  for (let i = 0; i < toolQueue.length; i++) {
1135
1156
  const item = toolQueue[i];
1136
1157
  if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
1158
+ const itemCheck = item.locale === (toolQueue[0]?.locale || null) ? toolCheck : await this.canStartCommand({ tool, locale: item.locale });
1137
1159
  const previousStatus = item.status;
1138
1160
  const previousReason = item.waitingReason;
1139
- const waitReason = toolCheck.reason || 'Waiting in queue';
1161
+ const waitReason = itemCheck.reason || lt('queue_waiting_in_queue', {}, { locale: item.locale });
1140
1162
  item.setWaiting(waitReason);
1141
1163
 
1142
1164
  // Update message if status/reason changed or it's time for periodic update
@@ -1283,14 +1305,15 @@ export class SolveQueue {
1283
1305
  * @see https://github.com/link-assistant/hive-mind/issues/1159
1284
1306
  * @see https://github.com/link-assistant/hive-mind/issues/1267
1285
1307
  */
1286
- async formatStatus() {
1308
+ async formatStatus(options = {}) {
1309
+ const locale = getLocale(options);
1287
1310
  // Always show per-tool breakdown for all known queues
1288
1311
  const externalProcessing = await this.getExternalProcessingSnapshot(Object.keys(this.queues));
1289
- let message = 'Queues\n';
1312
+ let message = `${lt('queues', {}, { locale })}\n`;
1290
1313
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
1291
1314
  const pending = toolQueue.length;
1292
1315
  const processing = externalProcessing.byTool[tool] || 0;
1293
- message += `${tool} (pending: ${pending}, processing: ${processing})\n`;
1316
+ message += `${tool} (${lt('queue_pending', {}, { locale })}: ${pending}, ${lt('queue_processing', {}, { locale })}: ${processing})\n`;
1294
1317
  }
1295
1318
 
1296
1319
  return message;
@@ -1320,39 +1343,40 @@ export class SolveQueue {
1320
1343
  * @see https://github.com/link-assistant/hive-mind/issues/1159
1321
1344
  * @see https://github.com/link-assistant/hive-mind/issues/1267
1322
1345
  */
1323
- async formatDetailedStatus() {
1346
+ async formatDetailedStatus(options = {}) {
1347
+ const locale = getLocale(options);
1324
1348
  const stats = this.getStats();
1325
1349
  const externalProcessing = await this.getExternalProcessingSnapshot(Object.keys(this.queues));
1326
1350
 
1327
1351
  // Get actual processing counts for each tool queue.
1328
1352
  // This combines pgrep with tracked isolation status so users see detached
1329
1353
  // screen-isolated work even when the direct AI CLI process count is lower.
1330
- let message = 'šŸ“‹ *Solve Queue Status*\n\n';
1354
+ let message = `šŸ“‹ *${lt('solve_queue_status', {}, { locale })}*\n\n`;
1331
1355
 
1332
1356
  // Show per-tool queue breakdown with items grouped by queue
1333
1357
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
1334
1358
  const pending = toolQueue.length;
1335
1359
  const processing = externalProcessing.byTool[tool] || 0;
1336
- message += `*${tool}* (pending: ${pending}, processing: ${processing})\n`;
1360
+ message += `*${tool}* (${lt('queue_pending', {}, { locale })}: ${pending}, ${lt('queue_processing', {}, { locale })}: ${processing})\n`;
1337
1361
 
1338
1362
  // Show first 5 queued items for this tool
1339
1363
  const displayItems = toolQueue.slice(0, 5);
1340
1364
  for (const item of displayItems) {
1341
- const waitTime = formatDuration(item.getWaitTime());
1342
- message += ` • ${item.url} (${item.status}, ${waitTime})\n`;
1365
+ const waitTime = formatDuration(item.getWaitTime(), { locale });
1366
+ message += ` • ${item.url} (${queueStatusLabel(item.status, locale)}, ${waitTime})\n`;
1343
1367
  if (item.waitingReason) {
1344
1368
  message += ` ā”” ${item.waitingReason}\n`;
1345
1369
  }
1346
1370
  }
1347
1371
  if (toolQueue.length > 5) {
1348
- message += ` ... and ${toolQueue.length - 5} more\n`;
1372
+ message += ` ... ${lt('queue_and_more', { count: toolQueue.length - 5 }, { locale })}\n`;
1349
1373
  }
1350
1374
 
1351
1375
  message += '\n';
1352
1376
  }
1353
1377
 
1354
1378
  // Summary stats
1355
- message += `Completed: ${stats.completed}, Failed: ${stats.failed}\n`;
1379
+ message += `${lt('queue_completed', {}, { locale })}: ${stats.completed}, ${lt('queue_failed', {}, { locale })}: ${stats.failed}\n`;
1356
1380
 
1357
1381
  return message;
1358
1382
  }
@@ -8,6 +8,7 @@
8
8
  // This reduces version gathering time from ~30-150s to ~5s (limited by slowest command).
9
9
 
10
10
  import { getVersion } from './version.lib.mjs';
11
+ import { t } from './i18n.lib.mjs';
11
12
  import { exec } from 'child_process';
12
13
  import { promisify } from 'util';
13
14
 
@@ -969,6 +970,40 @@ export async function getVersionInfo(verbose = false, processVersion = null) {
969
970
  * @param {string|null} version - Version string or null
970
971
  * @param {string} [key] - Tool key for version parser lookup
971
972
  */
973
+ const ENGLISH_VERSION_LABELS = {
974
+ ai_agents: 'AI Agents',
975
+ browsers: 'Browsers',
976
+ browser_automation: 'Browser Automation',
977
+ connected: 'connected',
978
+ development_tools: 'Development Tools',
979
+ environment: 'Environment',
980
+ architecture: 'Architecture',
981
+ kernel: 'Kernel',
982
+ not_connected: 'not connected',
983
+ os: 'OS',
984
+ platform: 'Platform',
985
+ process_running_restart_needed: 'Process running: `{{processVersion}}` (restart needed)',
986
+ system: 'System',
987
+ version: 'Version',
988
+ };
989
+
990
+ function resolveVersionLocale(options = {}) {
991
+ if (typeof options === 'string') return options;
992
+ return options?.locale || null;
993
+ }
994
+
995
+ function vt(key, params = {}, options = {}) {
996
+ const fullKey = `version.${key}`;
997
+ const locale = resolveVersionLocale(options);
998
+ const value = t(fullKey, params, locale ? { locale } : {});
999
+ if (value !== fullKey) return value;
1000
+ let fallback = ENGLISH_VERSION_LABELS[key] || key;
1001
+ for (const [paramKey, paramValue] of Object.entries(params)) {
1002
+ fallback = fallback.replace(new RegExp(`{{${paramKey}}}`, 'g'), String(paramValue));
1003
+ }
1004
+ return fallback;
1005
+ }
1006
+
972
1007
  function addVersionLine(lines, label, version, key) {
973
1008
  if (version) {
974
1009
  const display = key ? parseVersion(key, version) : version;
@@ -982,15 +1017,17 @@ function addVersionLine(lines, label, version, key) {
982
1017
  * @param {Object} versions - Version information object
983
1018
  * @returns {string} Formatted message
984
1019
  */
985
- export function formatVersionMessage(versions) {
1020
+ export function formatVersionMessage(versions, options = {}) {
1021
+ const locale = resolveVersionLocale(options);
1022
+ const vOptions = { locale };
986
1023
  const lines = [];
987
1024
 
988
1025
  // === Hive-Mind Package (single entry with restart warning) ===
989
1026
  lines.push('*šŸ¤– Hive-Mind*');
990
1027
  if (versions.hiveMind) {
991
- lines.push(`• Version: \`${versions.hiveMind}\``);
1028
+ lines.push(`• ${vt('version', {}, vOptions)}: \`${versions.hiveMind}\``);
992
1029
  if (versions.needsRestart) {
993
- lines.push(`āš ļø _Process running: \`${versions.processVersion}\` (restart needed)_`);
1030
+ lines.push(`āš ļø _${vt('process_running_restart_needed', { processVersion: versions.processVersion }, vOptions)}_`);
994
1031
  }
995
1032
  }
996
1033
 
@@ -1006,7 +1043,7 @@ export function formatVersionMessage(versions) {
1006
1043
 
1007
1044
  if (agentLines.length > 0) {
1008
1045
  lines.push('');
1009
- lines.push('*šŸŽ­ AI Agents*');
1046
+ lines.push(`*šŸŽ­ ${vt('ai_agents', {}, vOptions)}*`);
1010
1047
  lines.push(...agentLines);
1011
1048
  }
1012
1049
 
@@ -1173,7 +1210,7 @@ export function formatVersionMessage(versions) {
1173
1210
 
1174
1211
  if (browserLines.length > 0) {
1175
1212
  lines.push('');
1176
- lines.push('*🌐 Browsers*');
1213
+ lines.push(`*🌐 ${vt('browsers', {}, vOptions)}*`);
1177
1214
  lines.push(...browserLines);
1178
1215
  }
1179
1216
 
@@ -1184,15 +1221,15 @@ export function formatVersionMessage(versions) {
1184
1221
  // Playwright MCP: show version with Claude Code and Codex connection status inline
1185
1222
  if (versions.playwrightMcp) {
1186
1223
  const mcpVersion = parseVersion('playwrightMcp', versions.playwrightMcp);
1187
- const claudeStatus = versions.playwrightMcpClaudeStatus ? 'connected' : 'not connected';
1188
- const codexStatus = versions.playwrightMcpCodexStatus ? 'connected' : 'not connected';
1224
+ const claudeStatus = versions.playwrightMcpClaudeStatus ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
1225
+ const codexStatus = versions.playwrightMcpCodexStatus ? vt('connected', {}, vOptions) : vt('not_connected', {}, vOptions);
1189
1226
  browserAutoLines.push(`• Playwright MCP: \`${mcpVersion} | Claude Code: ${claudeStatus} | Codex: ${codexStatus}\``);
1190
1227
  }
1191
1228
  addVersionLine(browserAutoLines, 'Puppeteer Browsers', versions.puppeteerBrowsers, 'puppeteerBrowsers');
1192
1229
 
1193
1230
  if (browserAutoLines.length > 0) {
1194
1231
  lines.push('');
1195
- lines.push('*šŸŽ­ Browser Automation*');
1232
+ lines.push(`*šŸŽ­ ${vt('browser_automation', {}, vOptions)}*`);
1196
1233
  lines.push(...browserAutoLines);
1197
1234
  }
1198
1235
 
@@ -1212,24 +1249,24 @@ export function formatVersionMessage(versions) {
1212
1249
 
1213
1250
  if (toolLines.length > 0) {
1214
1251
  lines.push('');
1215
- lines.push('*šŸ›  Development Tools*');
1252
+ lines.push(`*šŸ›  ${vt('development_tools', {}, vOptions)}*`);
1216
1253
  lines.push(...toolLines);
1217
1254
  }
1218
1255
 
1219
1256
  // === Platform (detailed) ===
1220
1257
  {
1221
1258
  const platformLines = [];
1222
- if (versions.platformEnvironment) platformLines.push(`• Environment: \`${versions.platformEnvironment}\``);
1223
- if (versions.platformArch) platformLines.push(`• Architecture: \`${versions.platformArch}\``);
1224
- if (versions.platformOs) platformLines.push(`• OS: \`${versions.platformOs}\``);
1225
- if (versions.platformKernel) platformLines.push(`• Kernel: \`${versions.platformKernel}\``);
1259
+ if (versions.platformEnvironment) platformLines.push(`• ${vt('environment', {}, vOptions)}: \`${versions.platformEnvironment}\``);
1260
+ if (versions.platformArch) platformLines.push(`• ${vt('architecture', {}, vOptions)}: \`${versions.platformArch}\``);
1261
+ if (versions.platformOs) platformLines.push(`• ${vt('os', {}, vOptions)}: \`${versions.platformOs}\``);
1262
+ if (versions.platformKernel) platformLines.push(`• ${vt('kernel', {}, vOptions)}: \`${versions.platformKernel}\``);
1226
1263
  // Fallback to legacy single-line format
1227
1264
  if (platformLines.length === 0 && versions.platform) {
1228
- platformLines.push(`• System: \`${versions.platform}\``);
1265
+ platformLines.push(`• ${vt('system', {}, vOptions)}: \`${versions.platform}\``);
1229
1266
  }
1230
1267
  if (platformLines.length > 0) {
1231
1268
  lines.push('');
1232
- lines.push('*šŸ’» Platform*');
1269
+ lines.push(`*šŸ’» ${vt('platform', {}, vOptions)}*`);
1233
1270
  lines.push(...platformLines);
1234
1271
  }
1235
1272
  }