@deadragdoll/tellymcp 0.0.9 → 0.0.10

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 (27) hide show
  1. package/.env.example.client +2 -0
  2. package/.env.example.gateway +2 -0
  3. package/VERSION.md +26 -0
  4. package/config/templates/env.both.template +1 -0
  5. package/config/templates/env.client.template +1 -0
  6. package/config/templates/env.gateway.template +1 -0
  7. package/dist/services/features/telegram-mcp/approval.service.js +1 -1
  8. package/dist/services/features/telegram-mcp/browser.service.js +1 -1
  9. package/dist/services/features/telegram-mcp/collaboration.service.js +1 -1
  10. package/dist/services/features/telegram-mcp/gateway-rmq.service.js +3 -2
  11. package/dist/services/features/telegram-mcp/gateway-socket.service.js +8 -2
  12. package/dist/services/features/telegram-mcp/inbox.service.js +1 -1
  13. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -1
  14. package/dist/services/features/telegram-mcp/notify.service.js +1 -1
  15. package/dist/services/features/telegram-mcp/pair.service.js +1 -1
  16. package/dist/services/features/telegram-mcp/runtime.service.js +27 -7
  17. package/dist/services/features/telegram-mcp/session-context.service.js +1 -1
  18. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +2 -1
  19. package/dist/services/features/telegram-mcp/src/app/config/env.js +4 -0
  20. package/dist/services/features/telegram-mcp/src/shared/i18n/index.js +46 -0
  21. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +555 -0
  22. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +555 -0
  23. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +9 -0
  24. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +1148 -575
  25. package/dist/services/features/telegram-mcp/standalone-http.service.js +1 -1
  26. package/dist/services/features/telegram-mcp/tools-sync.service.js +1 -1
  27. package/package.json +2 -1
@@ -18,6 +18,7 @@ const collabUi_1 = require("./collabUi");
18
18
  const proxyFetch_1 = require("./proxyFetch");
19
19
  const client_1 = require("../tmux/client");
20
20
  const versionHandshake_1 = require("../../lib/version/versionHandshake");
21
+ const i18n_1 = require("../../i18n");
21
22
  const LOCAL_INDEX_FILE_NAME = "LOCAL_INDEX.md";
22
23
  const TMUX_NUDGE_FAILURE_NOTICE_COOLDOWN_MS = 5 * 60 * 1000;
23
24
  function trimTrailingSlashes(value) {
@@ -278,6 +279,7 @@ class TelegramTransport {
278
279
  bindingStore;
279
280
  inboxStore;
280
281
  menuPayloadStore;
282
+ localeStore;
281
283
  xchangeFileMetaStore;
282
284
  maintenanceStore;
283
285
  objectStore;
@@ -326,18 +328,19 @@ class TelegramTransport {
326
328
  menuId: ctx.callbackQuery?.data ?? "unknown",
327
329
  });
328
330
  await ctx.answerCallbackQuery({
329
- text: "Menu refreshed.",
331
+ text: await this.tForContext(ctx, "common:menu.refreshed"),
330
332
  });
331
333
  await handler(ctx);
332
334
  },
333
335
  };
334
336
  }
335
- constructor(config, sessionStore, bindingStore, inboxStore, menuPayloadStore, xchangeFileMetaStore, maintenanceStore, objectStore, webAppLaunchRegistry, logger) {
337
+ constructor(config, sessionStore, bindingStore, inboxStore, menuPayloadStore, localeStore, xchangeFileMetaStore, maintenanceStore, objectStore, webAppLaunchRegistry, logger) {
336
338
  this.config = config;
337
339
  this.sessionStore = sessionStore;
338
340
  this.bindingStore = bindingStore;
339
341
  this.inboxStore = inboxStore;
340
342
  this.menuPayloadStore = menuPayloadStore;
343
+ this.localeStore = localeStore;
341
344
  this.xchangeFileMetaStore = xchangeFileMetaStore;
342
345
  this.maintenanceStore = maintenanceStore;
343
346
  this.objectStore = objectStore;
@@ -405,7 +408,9 @@ class TelegramTransport {
405
408
  await this.cancelPendingPartnerNote(ctx);
406
409
  });
407
410
  this.bot.callbackQuery("partner-back", async (ctx) => {
408
- await ctx.answerCallbackQuery({ text: "Назад к напарнику." });
411
+ await ctx.answerCallbackQuery({
412
+ text: await this.tForContext(ctx, "menu:partner.actions.back_to_partner"),
413
+ });
409
414
  await this.showPartnerMenu(ctx);
410
415
  });
411
416
  this.bot.callbackQuery("file-handoff-cancel", async (ctx) => {
@@ -814,26 +819,59 @@ class TelegramTransport {
814
819
  if (!primarySessionId) {
815
820
  continue;
816
821
  }
822
+ const locale = await this.resolveLocaleForTelegramUserId(recipientGroup.binding.telegramUserId);
817
823
  const uniqueSessionLabels = Array.from(new Set(recipientGroup.sessionLabels)).sort();
824
+ const browserStatus = this.config.browser.enabled
825
+ ? (this.config.browser.headless ? "enabled, headless" : "enabled, headed")
826
+ : "disabled";
818
827
  const startupMessage = [
819
- "✅ TellyMCP запущен.",
820
- `Версия: ${packageVersion}`,
821
- `Протокол: ${versionHandshake_1.TELLYMCP_PROTOCOL_VERSION}`,
822
- `Режим: ${this.config.distributed.mode}`,
828
+ this.t(locale, "menu:notices.startup.title"),
829
+ this.t(locale, "menu:notices.startup.version", {
830
+ packageVersion,
831
+ }),
832
+ this.t(locale, "menu:notices.startup.protocol", {
833
+ protocolVersion: versionHandshake_1.TELLYMCP_PROTOCOL_VERSION,
834
+ }),
835
+ this.t(locale, "menu:notices.startup.mode", {
836
+ mode: this.config.distributed.mode,
837
+ }),
823
838
  ...(this.config.telegram.botUsername
824
- ? [`Бот: @${this.config.telegram.botUsername.replace(/^@/u, "")}`]
839
+ ? [
840
+ this.t(locale, "menu:notices.startup.bot", {
841
+ botUsername: this.config.telegram.botUsername.replace(/^@/u, ""),
842
+ }),
843
+ ]
844
+ : []),
845
+ this.t(locale, "menu:notices.startup.sessions", {
846
+ count: uniqueSessionLabels.length,
847
+ }),
848
+ this.t(locale, "menu:notices.startup.session_list", {
849
+ sessions: uniqueSessionLabels.join(", "),
850
+ }),
851
+ this.t(locale, "menu:notices.startup.mcp", {
852
+ url: localMcpUrl,
853
+ }),
854
+ ...(this.config.webapp.enabled
855
+ ? [this.t(locale, "menu:notices.startup.webapp", { url: localWebappUrl })]
825
856
  : []),
826
- `Сессии: ${uniqueSessionLabels.join(", ")}`,
827
- `MCP: ${localMcpUrl}`,
828
- ...(this.config.webapp.enabled ? [`WebApp: ${localWebappUrl}`] : []),
829
857
  ...(this.config.distributed.gatewayPublicUrl
830
- ? [`Gateway: ${this.config.distributed.gatewayPublicUrl}`]
858
+ ? [
859
+ this.t(locale, "menu:notices.startup.gateway", {
860
+ url: this.config.distributed.gatewayPublicUrl,
861
+ }),
862
+ ]
831
863
  : []),
832
864
  ...(this.config.distributed.gatewayWsUrl
833
- ? [`Gateway WS: ${this.config.distributed.gatewayWsUrl}`]
865
+ ? [
866
+ this.t(locale, "menu:notices.startup.gateway_ws", {
867
+ url: this.config.distributed.gatewayWsUrl,
868
+ }),
869
+ ]
834
870
  : []),
835
- `Browser: ${this.config.browser.enabled ? (this.config.browser.headless ? "enabled, headless" : "enabled, headed") : "disabled"}`,
836
- "Напиши /menu, чтобы открыть меню сессий.",
871
+ this.t(locale, "menu:notices.startup.browser", {
872
+ status: browserStatus,
873
+ }),
874
+ this.t(locale, "menu:notices.startup.hint"),
837
875
  ].join("\n");
838
876
  try {
839
877
  await this.sendNotification({
@@ -900,10 +938,10 @@ class TelegramTransport {
900
938
  return { externalMessageId: response.messageId };
901
939
  }
902
940
  async handleProjectMemberJoinedEvent(input) {
903
- const memberLabel = input.member_display_name?.trim() ||
941
+ const rawMemberLabel = input.member_display_name?.trim() ||
904
942
  (input.member_telegram_username?.trim()
905
943
  ? `@${input.member_telegram_username.trim().replace(/^@/u, "")}`
906
- : "Новый участник");
944
+ : null);
907
945
  const sessions = await this.sessionStore.listSessions();
908
946
  const notifiedChats = new Set();
909
947
  for (const session of sessions) {
@@ -911,6 +949,8 @@ class TelegramTransport {
911
949
  if (!binding || notifiedChats.has(binding.telegramChatId)) {
912
950
  continue;
913
951
  }
952
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
953
+ const memberLabel = rawMemberLabel ?? this.t(locale, "menu:notices.project.new_member");
914
954
  await this.sendNotification({
915
955
  sessionId: session.sessionId,
916
956
  ...(session.label ? { sessionLabel: session.label } : {}),
@@ -918,16 +958,19 @@ class TelegramTransport {
918
958
  telegramChatId: binding.telegramChatId,
919
959
  telegramUserId: binding.telegramUserId,
920
960
  },
921
- message: проект «${input.project_name}» вошёл участник: ${memberLabel}.`,
961
+ message: this.t(locale, "menu:notices.project.member_joined", {
962
+ projectName: input.project_name,
963
+ memberLabel,
964
+ }),
922
965
  });
923
966
  notifiedChats.add(binding.telegramChatId);
924
967
  }
925
968
  }
926
969
  async handleProjectMemberLeftEvent(input) {
927
- const memberLabel = input.member_display_name?.trim() ||
970
+ const rawMemberLabel = input.member_display_name?.trim() ||
928
971
  (input.member_telegram_username?.trim()
929
972
  ? `@${input.member_telegram_username.trim().replace(/^@/u, "")}`
930
- : "Участник");
973
+ : null);
931
974
  const sessions = await this.sessionStore.listSessions();
932
975
  const notifiedChats = new Set();
933
976
  for (const session of sessions) {
@@ -935,6 +978,8 @@ class TelegramTransport {
935
978
  if (!binding || notifiedChats.has(binding.telegramChatId)) {
936
979
  continue;
937
980
  }
981
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
982
+ const memberLabel = rawMemberLabel ?? this.t(locale, "menu:notices.project.member");
938
983
  await this.sendNotification({
939
984
  sessionId: session.sessionId,
940
985
  ...(session.label ? { sessionLabel: session.label } : {}),
@@ -942,7 +987,10 @@ class TelegramTransport {
942
987
  telegramChatId: binding.telegramChatId,
943
988
  telegramUserId: binding.telegramUserId,
944
989
  },
945
- message: `Из проекта «${input.project_name}» вышел участник: ${memberLabel}.`,
990
+ message: this.t(locale, "menu:notices.project.member_left", {
991
+ projectName: input.project_name,
992
+ memberLabel,
993
+ }),
946
994
  });
947
995
  notifiedChats.add(binding.telegramChatId);
948
996
  }
@@ -963,6 +1011,7 @@ class TelegramTransport {
963
1011
  if (!binding || notifiedChats.has(binding.telegramChatId)) {
964
1012
  continue;
965
1013
  }
1014
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
966
1015
  await this.sendNotification({
967
1016
  sessionId: session.sessionId,
968
1017
  ...(session.label ? { sessionLabel: session.label } : {}),
@@ -970,7 +1019,9 @@ class TelegramTransport {
970
1019
  telegramChatId: binding.telegramChatId,
971
1020
  telegramUserId: binding.telegramUserId,
972
1021
  },
973
- message: `Проект «${input.project_name}» удалён. Локальные project bindings очищены.`,
1022
+ message: this.t(locale, "menu:notices.project.deleted", {
1023
+ projectName: input.project_name,
1024
+ }),
974
1025
  });
975
1026
  notifiedChats.add(binding.telegramChatId);
976
1027
  }
@@ -1055,9 +1106,11 @@ class TelegramTransport {
1055
1106
  telegramUserId: binding.telegramUserId,
1056
1107
  },
1057
1108
  message: [
1058
- "TOOLS.md обновлён на шлюзе или отсутствует локально.",
1059
- `Сессия: ${session.label ?? input.session_label ?? session.sessionId}`,
1060
- "Действие обязательно: вызови refresh_tools_markdown, затем перечитай локальный TOOLS.md и применяй его перед продолжением работы.",
1109
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.tools.changed"),
1110
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.tools.session", {
1111
+ sessionName: session.label ?? input.session_label ?? session.sessionId,
1112
+ }),
1113
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.tools.action_required"),
1061
1114
  ].join("\n"),
1062
1115
  });
1063
1116
  await this.sessionStore.setSession({
@@ -1124,12 +1177,20 @@ class TelegramTransport {
1124
1177
  telegramUserId: binding.telegramUserId,
1125
1178
  },
1126
1179
  message: [
1127
- input.compatibility === "reject"
1128
- ? "Шлюз и клиент несовместимы по протоколу. Транспорт этой сессии заблокирован."
1129
- : "Версии шлюза и клиента различаются.",
1130
- `Сессия: ${session.label ?? input.session_label ?? session.sessionId}`,
1131
- `Клиент: ${input.client_package_version} / protocol ${input.client_protocol_version}`,
1132
- `Шлюз: ${input.gateway_package_version} / protocol ${input.gateway_protocol_version}`,
1180
+ await this.tForTelegramUserId(binding.telegramUserId, input.compatibility === "reject"
1181
+ ? "menu:notices.version.reject"
1182
+ : "menu:notices.version.warn"),
1183
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.version.session", {
1184
+ sessionName: session.label ?? input.session_label ?? session.sessionId,
1185
+ }),
1186
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.version.client", {
1187
+ packageVersion: input.client_package_version,
1188
+ protocolVersion: input.client_protocol_version,
1189
+ }),
1190
+ await this.tForTelegramUserId(binding.telegramUserId, "menu:notices.version.gateway", {
1191
+ packageVersion: input.gateway_package_version,
1192
+ protocolVersion: input.gateway_protocol_version,
1193
+ }),
1133
1194
  input.instruction,
1134
1195
  ].join("\n"),
1135
1196
  });
@@ -1151,6 +1212,7 @@ class TelegramTransport {
1151
1212
  });
1152
1213
  return;
1153
1214
  }
1215
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
1154
1216
  const payloadKey = await this.createLiveApprovalMenuPayload({
1155
1217
  sessionId: targetSession.sessionId,
1156
1218
  sourceSessionId: input.source_session_id,
@@ -1165,17 +1227,27 @@ class TelegramTransport {
1165
1227
  ...(input.project_name ? { projectName: input.project_name } : {}),
1166
1228
  });
1167
1229
  const sent = await this.sendChatMessage(binding.telegramChatId, [
1168
- "🖥 Запрос Live View",
1230
+ this.t(locale, "menu:live.approval.request_title"),
1169
1231
  "",
1170
- ...(input.project_name ? [`Проект: ${input.project_name}`] : []),
1171
- `Сессия: ${input.source_session_label} -> ${input.target_session_label}`,
1232
+ ...(input.project_name
1233
+ ? [
1234
+ this.t(locale, "menu:live.approval.project", {
1235
+ projectName: input.project_name,
1236
+ }),
1237
+ ]
1238
+ : []),
1239
+ this.t(locale, "menu:live.approval.route", {
1240
+ sourceSessionName: input.source_session_label,
1241
+ targetSessionName: input.target_session_label,
1242
+ }),
1172
1243
  "",
1173
- `Сессия ${input.source_session_label} запрашивает просмотр Live вашей сессии.`,
1174
- "Разрешить доступ?",
1244
+ this.t(locale, "menu:live.approval.request_message", {
1245
+ sourceSessionName: input.source_session_label,
1246
+ }),
1175
1247
  ].join("\n"), {
1176
1248
  reply_markup: new grammy_1.InlineKeyboard()
1177
- .text("✅ Разрешить", `live-approval:approve:${payloadKey}`)
1178
- .text("❌ Отклонить", `live-approval:deny:${payloadKey}`),
1249
+ .text(`✅ ${this.t(locale, "menu:live.approval.approve")}`, `live-approval:approve:${payloadKey}`)
1250
+ .text(`❌ ${this.t(locale, "menu:live.approval.deny")}`, `live-approval:deny:${payloadKey}`),
1179
1251
  }, {
1180
1252
  kind: "notification",
1181
1253
  sessionId: targetSession.sessionId,
@@ -1205,6 +1277,7 @@ class TelegramTransport {
1205
1277
  });
1206
1278
  return;
1207
1279
  }
1280
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
1208
1281
  if (!input.approved) {
1209
1282
  await this.sendNotification({
1210
1283
  sessionId: sourceSession.sessionId,
@@ -1214,9 +1287,18 @@ class TelegramTransport {
1214
1287
  telegramUserId: binding.telegramUserId,
1215
1288
  },
1216
1289
  message: [
1217
- "🖥 Live View отклонён.",
1218
- ...(input.project_name ? [`Проект: ${input.project_name}`] : []),
1219
- `Сессия: ${input.source_session_label} -> ${input.target_session_label}`,
1290
+ this.t(locale, "menu:live.approval.denied"),
1291
+ ...(input.project_name
1292
+ ? [
1293
+ this.t(locale, "menu:live.approval.project", {
1294
+ projectName: input.project_name,
1295
+ }),
1296
+ ]
1297
+ : []),
1298
+ this.t(locale, "menu:live.approval.route", {
1299
+ sourceSessionName: input.source_session_label,
1300
+ targetSessionName: input.target_session_label,
1301
+ }),
1220
1302
  ].join("\n"),
1221
1303
  });
1222
1304
  return;
@@ -1231,12 +1313,21 @@ class TelegramTransport {
1231
1313
  throw new Error("Unable to build Live View URL for approved request.");
1232
1314
  }
1233
1315
  const sent = await this.sendChatMessage(binding.telegramChatId, [
1234
- "🖥 Live View разрешён",
1316
+ this.t(locale, "menu:live.approval.approved"),
1235
1317
  "",
1236
- ...(input.project_name ? [`Проект: ${input.project_name}`] : []),
1237
- `Сессия: ${input.source_session_label} -> ${input.target_session_label}`,
1318
+ ...(input.project_name
1319
+ ? [
1320
+ this.t(locale, "menu:live.approval.project", {
1321
+ projectName: input.project_name,
1322
+ }),
1323
+ ]
1324
+ : []),
1325
+ this.t(locale, "menu:live.approval.route", {
1326
+ sourceSessionName: input.source_session_label,
1327
+ targetSessionName: input.target_session_label,
1328
+ }),
1238
1329
  "",
1239
- "Открой Live в нужном режиме по кнопкам ниже.",
1330
+ this.t(locale, "menu:live.actions.choose_mode"),
1240
1331
  ].join("\n"), {
1241
1332
  reply_markup: this.buildLiveViewLaunchKeyboard((mode) => this.buildLiveViewUrlForSessionTarget({
1242
1333
  targetSessionId: input.target_session_id,
@@ -1244,7 +1335,7 @@ class TelegramTransport {
1244
1335
  targetLocalSessionId: input.target_local_session_id,
1245
1336
  sourceClientUuid: input.source_client_uuid,
1246
1337
  launchMode: mode,
1247
- })),
1338
+ }), locale),
1248
1339
  }, {
1249
1340
  kind: "notification",
1250
1341
  sessionId: sourceSession.sessionId,
@@ -1475,22 +1566,26 @@ class TelegramTransport {
1475
1566
  fingerprint: async (ctx) => this.buildMainMenuFingerprint(ctx),
1476
1567
  ...this.createMenuOptions((ctx) => this.showMainMenu(ctx)),
1477
1568
  })
1478
- .text("🖥 Live", async (ctx) => {
1569
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.live"), async (ctx) => {
1479
1570
  await this.showLiveViewLauncher(ctx);
1480
1571
  })
1481
- .text("📄 Content", async (ctx) => {
1482
- await ctx.answerCallbackQuery({ text: "Opening content menu." });
1572
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.content"), async (ctx) => {
1573
+ await ctx.answerCallbackQuery({
1574
+ text: await this.tForContext(ctx, "menu:main.actions.open_content"),
1575
+ });
1483
1576
  await this.showBufferMenu(ctx);
1484
1577
  })
1485
- .text("🌐 Browser", async (ctx) => {
1486
- await ctx.answerCallbackQuery({ text: "Opening browser menu." });
1578
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.browser"), async (ctx) => {
1579
+ await ctx.answerCallbackQuery({
1580
+ text: await this.tForContext(ctx, "menu:main.actions.open_browser"),
1581
+ });
1487
1582
  await this.showBrowserMenu(ctx);
1488
1583
  })
1489
1584
  .row()
1490
- .text("🏠 Local", async (ctx) => {
1585
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.local"), async (ctx) => {
1491
1586
  await this.showLocalEntryPoint(ctx);
1492
1587
  })
1493
- .text("👥 Collab", async (ctx) => {
1588
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.collab"), async (ctx) => {
1494
1589
  await this.showProjectsEntryPoint(ctx);
1495
1590
  })
1496
1591
  .row()
@@ -1499,32 +1594,44 @@ class TelegramTransport {
1499
1594
  chatId: ctx.chat?.id,
1500
1595
  userId: ctx.from?.id,
1501
1596
  });
1502
- await ctx.answerCallbackQuery({ text: "Opening inbox." });
1597
+ await ctx.answerCallbackQuery({
1598
+ text: await this.tForContext(ctx, "menu:main.actions.open_inbox"),
1599
+ });
1503
1600
  await this.showInboxMenu(ctx);
1504
1601
  })
1505
- .text("📦 Storage", async (ctx) => {
1506
- await ctx.answerCallbackQuery({ text: "Открываю storage." });
1602
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.storage"), async (ctx) => {
1603
+ await ctx.answerCallbackQuery({
1604
+ text: await this.tForContext(ctx, "menu:main.actions.open_storage"),
1605
+ });
1507
1606
  await this.showStorageMenu(ctx);
1508
1607
  })
1509
- .text("⚙ Settings", async (ctx) => {
1510
- await ctx.answerCallbackQuery({ text: "Открываю настройки." });
1608
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.settings"), async (ctx) => {
1609
+ await ctx.answerCallbackQuery({
1610
+ text: await this.tForContext(ctx, "menu:main.actions.open_settings"),
1611
+ });
1511
1612
  await this.showSettingsMenu(ctx);
1512
1613
  })
1513
1614
  .row()
1514
- .text("⬅ Back", async (ctx) => {
1515
- await ctx.answerCallbackQuery({ text: "Back to sessions." });
1615
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.back"), async (ctx) => {
1616
+ await ctx.answerCallbackQuery({
1617
+ text: await this.tForContext(ctx, "menu:main.actions.back_to_sessions"),
1618
+ });
1516
1619
  await this.showSessionsMenu(ctx);
1517
1620
  });
1518
1621
  }
1519
1622
  createBrowserMenu() {
1520
1623
  return new menu_1.Menu("telegram-browser-menu", this.createMenuOptions((ctx) => this.showBrowserMenu(ctx)))
1521
1624
  .text(async (ctx) => this.buildScreenshotsButtonLabel(ctx), async (ctx) => {
1522
- await ctx.answerCallbackQuery({ text: "Opening screenshots." });
1625
+ await ctx.answerCallbackQuery({
1626
+ text: await this.tForContext(ctx, "menu:browser.actions.open_screenshots"),
1627
+ });
1523
1628
  await this.showScreenshotsMenu(ctx);
1524
1629
  })
1525
1630
  .row()
1526
- .text("⬅ Back", async (ctx) => {
1527
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
1631
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1632
+ await ctx.answerCallbackQuery({
1633
+ text: await this.tForContext(ctx, "menu:browser.actions.back_to_session_menu"),
1634
+ });
1528
1635
  await this.showMainMenu(ctx);
1529
1636
  });
1530
1637
  }
@@ -1537,9 +1644,9 @@ class TelegramTransport {
1537
1644
  const range = new menu_1.MenuRange();
1538
1645
  const { session, projects } = await this.loadProjectsContext(ctx);
1539
1646
  if (!session || !projects) {
1540
- range.text("Gateway недоступен", async (innerCtx) => {
1647
+ range.text(await this.tForContext(ctx, "common:menu.gateway_unavailable"), async (innerCtx) => {
1541
1648
  await innerCtx.answerCallbackQuery({
1542
- text: "Проекты доступны только через gateway.",
1649
+ text: await this.tForContext(innerCtx, "menu:collab.actions.gateway_only"),
1543
1650
  show_alert: true,
1544
1651
  });
1545
1652
  });
@@ -1547,9 +1654,9 @@ class TelegramTransport {
1547
1654
  }
1548
1655
  if (projects.length === 0) {
1549
1656
  range
1550
- .text("🫥 Нет проектов", async (innerCtx) => {
1657
+ .text(await this.tForContext(ctx, "menu:collab.labels.no_projects"), async (innerCtx) => {
1551
1658
  await innerCtx.answerCallbackQuery({
1552
- text: "Проектов пока нет. Создай или войди в существующий.",
1659
+ text: await this.tForContext(innerCtx, "menu:collab.actions.no_projects"),
1553
1660
  });
1554
1661
  })
1555
1662
  .row();
@@ -1568,38 +1675,46 @@ class TelegramTransport {
1568
1675
  }
1569
1676
  return range;
1570
1677
  })
1571
- .text("➕ Создать", async (ctx) => {
1678
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.create"), async (ctx) => {
1572
1679
  await this.beginProjectMode(ctx, "create");
1573
1680
  })
1574
- .text("🛠 Tools", async (ctx) => {
1575
- await ctx.answerCallbackQuery({ text: "Открываю инструменты проекта." });
1681
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.tools"), async (ctx) => {
1682
+ await ctx.answerCallbackQuery({
1683
+ text: await this.tForContext(ctx, "menu:collab.actions.open_tools"),
1684
+ });
1576
1685
  await this.showCollabToolsMenu(ctx);
1577
1686
  })
1578
1687
  .row()
1579
- .text("🔑 Войти", async (ctx) => {
1688
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.join"), async (ctx) => {
1580
1689
  await this.beginProjectMode(ctx, "join");
1581
1690
  })
1582
- .text("⬅ Назад", async (ctx) => {
1583
- await ctx.answerCallbackQuery({ text: "Назад к меню сессии." });
1691
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1692
+ await ctx.answerCallbackQuery({
1693
+ text: await this.tForContext(ctx, "menu:collab.actions.back_to_session_menu"),
1694
+ });
1584
1695
  await this.showMainMenu(ctx);
1585
1696
  });
1586
1697
  }
1587
1698
  createCollabToolsMenu() {
1588
1699
  return new menu_1.Menu("telegram-collab-tools-menu", this.createMenuOptions((ctx) => this.showCollabToolsMenu(ctx)))
1589
- .text("📣 Broadcast", async (ctx) => {
1700
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.broadcast"), async (ctx) => {
1590
1701
  await this.beginProjectBroadcast(ctx);
1591
1702
  })
1592
- .text("🕘 History", async (ctx) => {
1703
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.history"), async (ctx) => {
1593
1704
  await this.handleCollabHistoryExport(ctx);
1594
1705
  })
1595
1706
  .row()
1596
- .text("🗑 Delete", async (ctx) => {
1597
- await ctx.answerCallbackQuery({ text: "Открываю удаление проектов." });
1707
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.delete"), async (ctx) => {
1708
+ await ctx.answerCallbackQuery({
1709
+ text: await this.tForContext(ctx, "menu:collab.actions.open_delete"),
1710
+ });
1598
1711
  await this.showCollabDeleteMenu(ctx);
1599
1712
  })
1600
1713
  .row()
1601
- .text("⬅ Back", async (ctx) => {
1602
- await ctx.answerCallbackQuery({ text: "Назад к Collab." });
1714
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1715
+ await ctx.answerCallbackQuery({
1716
+ text: await this.tForContext(ctx, "menu:collab.actions.back_to_collab"),
1717
+ });
1603
1718
  await this.showProjectsMenu(ctx);
1604
1719
  });
1605
1720
  }
@@ -1612,18 +1727,18 @@ class TelegramTransport {
1612
1727
  const range = new menu_1.MenuRange();
1613
1728
  const { session, projects } = await this.loadProjectsContext(ctx);
1614
1729
  if (!session || !projects) {
1615
- range.text("Gateway недоступен", async (innerCtx) => {
1730
+ range.text(await this.tForContext(ctx, "common:menu.gateway_unavailable"), async (innerCtx) => {
1616
1731
  await innerCtx.answerCallbackQuery({
1617
- text: "Удаление проектов доступно только через gateway.",
1732
+ text: await this.tForContext(innerCtx, "menu:collab.actions.gateway_only"),
1618
1733
  show_alert: true,
1619
1734
  });
1620
1735
  });
1621
1736
  return range;
1622
1737
  }
1623
1738
  if (projects.length === 0) {
1624
- range.text("🫥 Нет проектов", async (innerCtx) => {
1739
+ range.text(await this.tForContext(ctx, "menu:collab.labels.no_projects"), async (innerCtx) => {
1625
1740
  await innerCtx.answerCallbackQuery({
1626
- text: "Удалять пока нечего.",
1741
+ text: await this.tForContext(innerCtx, "menu:collab.actions.no_projects"),
1627
1742
  });
1628
1743
  });
1629
1744
  return range;
@@ -1641,22 +1756,26 @@ class TelegramTransport {
1641
1756
  }
1642
1757
  return range;
1643
1758
  })
1644
- .text("⬅ Back", async (ctx) => {
1645
- await ctx.answerCallbackQuery({ text: "Назад к инструментам Collab." });
1759
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1760
+ await ctx.answerCallbackQuery({
1761
+ text: await this.tForContext(ctx, "menu:collab.actions.back_to_tools"),
1762
+ });
1646
1763
  await this.showCollabToolsMenu(ctx);
1647
1764
  });
1648
1765
  }
1649
1766
  createLocalMenu() {
1650
1767
  return new menu_1.Menu("telegram-local-menu", this.createMenuOptions((ctx) => this.showLocalMenu(ctx)))
1651
- .text("🤝 Напарник", async (ctx) => {
1768
+ .text(async (ctx) => this.tForContext(ctx, "menu:local.buttons.partner"), async (ctx) => {
1652
1769
  await this.showPartnerEntryPoint(ctx);
1653
1770
  })
1654
1771
  .text(async (ctx) => this.buildLinkButtonLabel(ctx), async (ctx) => {
1655
1772
  await this.handleLinkButton(ctx);
1656
1773
  })
1657
1774
  .row()
1658
- .text("⬅ Назад", async (ctx) => {
1659
- await ctx.answerCallbackQuery({ text: "Назад к меню сессии." });
1775
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1776
+ await ctx.answerCallbackQuery({
1777
+ text: await this.tForContext(ctx, "menu:local.actions.back_to_session_menu"),
1778
+ });
1660
1779
  await this.showMainMenu(ctx);
1661
1780
  });
1662
1781
  }
@@ -1669,9 +1788,9 @@ class TelegramTransport {
1669
1788
  const range = new menu_1.MenuRange();
1670
1789
  const principal = this.getPrincipalFromContext(ctx);
1671
1790
  if (!principal) {
1672
- range.text("No Telegram identity", async (innerCtx) => {
1791
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1673
1792
  await innerCtx.answerCallbackQuery({
1674
- text: "Telegram user or chat is missing.",
1793
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1675
1794
  show_alert: true,
1676
1795
  });
1677
1796
  });
@@ -1679,9 +1798,9 @@ class TelegramTransport {
1679
1798
  }
1680
1799
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1681
1800
  if (!activeSessionId) {
1682
- range.text("No active session", async (innerCtx) => {
1801
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1683
1802
  await innerCtx.answerCallbackQuery({
1684
- text: "No active session is linked yet.",
1803
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1685
1804
  show_alert: true,
1686
1805
  });
1687
1806
  });
@@ -1691,9 +1810,9 @@ class TelegramTransport {
1691
1810
  .filter((sessionId) => sessionId !== activeSessionId)
1692
1811
  .sort();
1693
1812
  if (sessionIds.length === 0) {
1694
- range.text("🫥 No partner sessions", async (innerCtx) => {
1813
+ range.text(await this.tForContext(ctx, "menu:link.labels.no_partner_sessions"), async (innerCtx) => {
1695
1814
  await innerCtx.answerCallbackQuery({
1696
- text: "No other linked sessions are available.",
1815
+ text: await this.tForContext(innerCtx, "menu:link.actions.no_partner_sessions"),
1697
1816
  show_alert: true,
1698
1817
  });
1699
1818
  });
@@ -1712,72 +1831,82 @@ class TelegramTransport {
1712
1831
  }
1713
1832
  return range;
1714
1833
  })
1715
- .text("⬅ Back", async (ctx) => {
1716
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
1834
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1835
+ await ctx.answerCallbackQuery({
1836
+ text: await this.tForContext(ctx, "menu:link.actions.back_to_session_menu"),
1837
+ });
1717
1838
  await this.showMainMenu(ctx);
1718
1839
  });
1719
1840
  }
1720
1841
  createPartnerMenu() {
1721
1842
  return new menu_1.Menu("telegram-partner-menu", this.createMenuOptions((ctx) => this.showPartnerMenu(ctx)))
1722
- .text("❓ Ask", async (ctx) => {
1843
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.ask"), async (ctx) => {
1723
1844
  await this.beginPartnerNoteMode(ctx, "question");
1724
1845
  })
1725
- .text("📤 Share", async (ctx) => {
1846
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.share"), async (ctx) => {
1726
1847
  await this.beginPartnerNoteMode(ctx, "share");
1727
1848
  })
1728
1849
  .row()
1729
- .text("🔓 Unlink", async (ctx) => {
1850
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.unlink"), async (ctx) => {
1730
1851
  await this.handleLinkButton(ctx);
1731
1852
  })
1732
- .text("⬅ Back", async (ctx) => {
1733
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
1853
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1854
+ await ctx.answerCallbackQuery({
1855
+ text: await this.tForContext(ctx, "menu:partner.actions.back_to_session_menu"),
1856
+ });
1734
1857
  await this.showMainMenu(ctx);
1735
1858
  });
1736
1859
  }
1737
1860
  createBufferMenu() {
1738
1861
  return new menu_1.Menu("telegram-buffer-menu", this.createMenuOptions((ctx) => this.showBufferMenu(ctx)))
1739
- .text("👁 Visible", async (ctx) => {
1862
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.visible"), async (ctx) => {
1740
1863
  await this.sendActiveSessionBuffer(ctx, { mode: "visible" });
1741
1864
  })
1742
- .text("🧾 Full", async (ctx) => {
1865
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.full"), async (ctx) => {
1743
1866
  await this.sendActiveSessionBuffer(ctx, { mode: "full" });
1744
1867
  })
1745
1868
  .row()
1746
- .text("📄 Last 300", async (ctx) => {
1869
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.last_300"), async (ctx) => {
1747
1870
  await this.sendActiveSessionBuffer(ctx, {
1748
1871
  mode: "lines",
1749
1872
  lines: 300,
1750
1873
  });
1751
1874
  })
1752
- .text("📄 Last 1000", async (ctx) => {
1875
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.last_1000"), async (ctx) => {
1753
1876
  await this.sendActiveSessionBuffer(ctx, {
1754
1877
  mode: "lines",
1755
1878
  lines: 1000,
1756
1879
  });
1757
1880
  })
1758
1881
  .row()
1759
- .text("⬅ Back", async (ctx) => {
1760
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
1882
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1883
+ await ctx.answerCallbackQuery({
1884
+ text: await this.tForContext(ctx, "menu:local.actions.back_to_session_menu"),
1885
+ });
1761
1886
  await this.showMainMenu(ctx);
1762
1887
  });
1763
1888
  }
1764
1889
  createSettingsMenu() {
1765
1890
  return new menu_1.Menu("telegram-settings-menu", this.createMenuOptions((ctx) => this.showSettingsMenu(ctx)))
1766
- .text("ℹ Info", async (ctx) => {
1891
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.info"), async (ctx) => {
1767
1892
  await this.showActiveSessionInfo(ctx);
1768
1893
  })
1769
1894
  .row()
1770
- .text("✏ Rename", async (ctx) => {
1895
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.rename"), async (ctx) => {
1771
1896
  await this.beginRenameActiveSession(ctx);
1772
1897
  })
1773
1898
  .row()
1774
- .text("🗑 Unpair", async (ctx) => {
1775
- await ctx.answerCallbackQuery({ text: "Confirm unpair." });
1899
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.unpair"), async (ctx) => {
1900
+ await ctx.answerCallbackQuery({
1901
+ text: await this.tForContext(ctx, "menu:settings.actions.confirm_unpair"),
1902
+ });
1776
1903
  await this.showUnpairConfirmMenu(ctx);
1777
1904
  })
1778
1905
  .row()
1779
- .text("⬅ Back", async (ctx) => {
1780
- await ctx.answerCallbackQuery({ text: "Назад к меню сессии." });
1906
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1907
+ await ctx.answerCallbackQuery({
1908
+ text: await this.tForContext(ctx, "menu:local.actions.back_to_session_menu"),
1909
+ });
1781
1910
  await this.showMainMenu(ctx);
1782
1911
  });
1783
1912
  }
@@ -1799,12 +1928,14 @@ class TelegramTransport {
1799
1928
  }
1800
1929
  createUnpairConfirmMenu() {
1801
1930
  return new menu_1.Menu("telegram-unpair-confirm-menu", this.createMenuOptions((ctx) => this.showUnpairConfirmMenu(ctx)))
1802
- .text("⚠ Confirm unpair", async (ctx) => {
1931
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.confirm_unpair"), async (ctx) => {
1803
1932
  await this.unpairActiveSession(ctx);
1804
1933
  })
1805
1934
  .row()
1806
- .text("⬅ Back", async (ctx) => {
1807
- await ctx.answerCallbackQuery({ text: "Back to settings." });
1935
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
1936
+ await ctx.answerCallbackQuery({
1937
+ text: await this.tForContext(ctx, "menu:settings.actions.back_to_settings"),
1938
+ });
1808
1939
  await this.showSettingsMenu(ctx);
1809
1940
  });
1810
1941
  }
@@ -1828,9 +1959,9 @@ class TelegramTransport {
1828
1959
  const range = new menu_1.MenuRange();
1829
1960
  const principal = this.getPrincipalFromContext(ctx);
1830
1961
  if (!principal) {
1831
- range.text("No Telegram identity", async (innerCtx) => {
1962
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1832
1963
  await innerCtx.answerCallbackQuery({
1833
- text: "Telegram user or chat is missing.",
1964
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1834
1965
  show_alert: true,
1835
1966
  });
1836
1967
  });
@@ -1838,9 +1969,9 @@ class TelegramTransport {
1838
1969
  }
1839
1970
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1840
1971
  if (!sessionId) {
1841
- range.text("No active session", async (innerCtx) => {
1972
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1842
1973
  await innerCtx.answerCallbackQuery({
1843
- text: "No active session is linked yet.",
1974
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1844
1975
  show_alert: true,
1845
1976
  });
1846
1977
  });
@@ -1848,9 +1979,9 @@ class TelegramTransport {
1848
1979
  }
1849
1980
  const inboxMessages = await this.inboxStore.listInboxMessages(sessionId, 10);
1850
1981
  if (inboxMessages.length === 0) {
1851
- range.text("📭 Inbox is empty", async (innerCtx) => {
1982
+ range.text(await this.tForContext(ctx, "menu:inbox.labels.empty"), async (innerCtx) => {
1852
1983
  await innerCtx.answerCallbackQuery({
1853
- text: "No unsolicited Telegram messages are stored.",
1984
+ text: await this.tForContext(innerCtx, "menu:inbox.actions.empty"),
1854
1985
  show_alert: false,
1855
1986
  });
1856
1987
  });
@@ -1868,12 +1999,16 @@ class TelegramTransport {
1868
1999
  }
1869
2000
  return range;
1870
2001
  })
1871
- .text("🔄 Refresh", async (ctx) => {
1872
- await ctx.answerCallbackQuery({ text: "Inbox refreshed." });
2002
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.refresh"), async (ctx) => {
2003
+ await ctx.answerCallbackQuery({
2004
+ text: await this.tForContext(ctx, "menu:inbox.actions.refreshed"),
2005
+ });
1873
2006
  await this.showInboxMenu(ctx);
1874
2007
  })
1875
- .text("⬅ Back", async (ctx) => {
1876
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
2008
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
2009
+ await ctx.answerCallbackQuery({
2010
+ text: await this.tForContext(ctx, "menu:local.actions.back_to_session_menu"),
2011
+ });
1877
2012
  await this.showMainMenu(ctx);
1878
2013
  });
1879
2014
  }
@@ -1886,9 +2021,9 @@ class TelegramTransport {
1886
2021
  const range = new menu_1.MenuRange();
1887
2022
  const principal = this.getPrincipalFromContext(ctx);
1888
2023
  if (!principal) {
1889
- range.text("No Telegram identity", async (innerCtx) => {
2024
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1890
2025
  await innerCtx.answerCallbackQuery({
1891
- text: "Telegram user or chat is missing.",
2026
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1892
2027
  show_alert: true,
1893
2028
  });
1894
2029
  });
@@ -1896,9 +2031,9 @@ class TelegramTransport {
1896
2031
  }
1897
2032
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1898
2033
  if (!sessionId) {
1899
- range.text("No active session", async (innerCtx) => {
2034
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1900
2035
  await innerCtx.answerCallbackQuery({
1901
- text: "No active session is linked yet.",
2036
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1902
2037
  show_alert: true,
1903
2038
  });
1904
2039
  });
@@ -1906,9 +2041,9 @@ class TelegramTransport {
1906
2041
  }
1907
2042
  const entries = await this.listActiveSessionStorageEntries(sessionId);
1908
2043
  if (entries.length === 0) {
1909
- range.text("📭 Storage is empty", async (innerCtx) => {
2044
+ range.text(await this.tForContext(ctx, "menu:storage.labels.empty"), async (innerCtx) => {
1910
2045
  await innerCtx.answerCallbackQuery({
1911
- text: .mcp-xchange пока нет файлов для этой сессии.",
2046
+ text: await this.tForContext(innerCtx, "menu:storage.actions.empty"),
1912
2047
  });
1913
2048
  });
1914
2049
  return range;
@@ -1925,12 +2060,16 @@ class TelegramTransport {
1925
2060
  }
1926
2061
  return range;
1927
2062
  })
1928
- .text("🔄 Refresh", async (ctx) => {
1929
- await ctx.answerCallbackQuery({ text: "Storage refreshed." });
2063
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.refresh"), async (ctx) => {
2064
+ await ctx.answerCallbackQuery({
2065
+ text: await this.tForContext(ctx, "menu:storage.actions.refreshed"),
2066
+ });
1930
2067
  await this.showStorageMenu(ctx);
1931
2068
  })
1932
- .text("⬅ Back", async (ctx) => {
1933
- await ctx.answerCallbackQuery({ text: "Back to session menu." });
2069
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
2070
+ await ctx.answerCallbackQuery({
2071
+ text: await this.tForContext(ctx, "menu:local.actions.back_to_session_menu"),
2072
+ });
1934
2073
  await this.showMainMenu(ctx);
1935
2074
  });
1936
2075
  }
@@ -1943,9 +2082,9 @@ class TelegramTransport {
1943
2082
  const range = new menu_1.MenuRange();
1944
2083
  const principal = this.getPrincipalFromContext(ctx);
1945
2084
  if (!principal) {
1946
- range.text("No Telegram identity", async (innerCtx) => {
2085
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1947
2086
  await innerCtx.answerCallbackQuery({
1948
- text: "Telegram user or chat is missing.",
2087
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1949
2088
  show_alert: true,
1950
2089
  });
1951
2090
  });
@@ -1953,9 +2092,9 @@ class TelegramTransport {
1953
2092
  }
1954
2093
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1955
2094
  if (!sessionId) {
1956
- range.text("No active session", async (innerCtx) => {
2095
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1957
2096
  await innerCtx.answerCallbackQuery({
1958
- text: "No active session is linked yet.",
2097
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1959
2098
  show_alert: true,
1960
2099
  });
1961
2100
  });
@@ -1963,9 +2102,9 @@ class TelegramTransport {
1963
2102
  }
1964
2103
  const filePaths = await this.listActiveSessionScreenshots(sessionId);
1965
2104
  if (filePaths.length === 0) {
1966
- range.text("📭 No screenshots", async (innerCtx) => {
2105
+ range.text(await this.tForContext(ctx, "menu:screenshots.labels.empty"), async (innerCtx) => {
1967
2106
  await innerCtx.answerCallbackQuery({
1968
- text: "No screenshots are stored for this session.",
2107
+ text: await this.tForContext(innerCtx, "menu:screenshots.actions.empty"),
1969
2108
  });
1970
2109
  });
1971
2110
  return range;
@@ -1982,12 +2121,16 @@ class TelegramTransport {
1982
2121
  }
1983
2122
  return range;
1984
2123
  })
1985
- .text("🔄 Refresh", async (ctx) => {
1986
- await ctx.answerCallbackQuery({ text: "Screenshots refreshed." });
2124
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.refresh"), async (ctx) => {
2125
+ await ctx.answerCallbackQuery({
2126
+ text: await this.tForContext(ctx, "menu:screenshots.actions.refreshed"),
2127
+ });
1987
2128
  await this.showScreenshotsMenu(ctx);
1988
2129
  })
1989
- .text("⬅ Back", async (ctx) => {
1990
- await ctx.answerCallbackQuery({ text: "Back to browser menu." });
2130
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
2131
+ await ctx.answerCallbackQuery({
2132
+ text: await this.tForContext(ctx, "menu:browser.actions.back_to_browser_menu"),
2133
+ });
1991
2134
  await this.showBrowserMenu(ctx);
1992
2135
  });
1993
2136
  }
@@ -2001,9 +2144,9 @@ class TelegramTransport {
2001
2144
  try {
2002
2145
  const principal = this.getPrincipalFromContext(ctx);
2003
2146
  if (!principal) {
2004
- range.text("No Telegram identity", async (innerCtx) => {
2147
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
2005
2148
  await innerCtx.answerCallbackQuery({
2006
- text: "Telegram user or chat is missing.",
2149
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
2007
2150
  show_alert: true,
2008
2151
  });
2009
2152
  });
@@ -2012,9 +2155,9 @@ class TelegramTransport {
2012
2155
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2013
2156
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
2014
2157
  if (sessionIds.length === 0) {
2015
- range.text("🫥 No linked sessions", async (innerCtx) => {
2158
+ range.text(await this.tForContext(ctx, "menu:sessions.labels.no_linked_sessions"), async (innerCtx) => {
2016
2159
  await innerCtx.answerCallbackQuery({
2017
- text: "No linked sessions found for this Telegram identity.",
2160
+ text: await this.tForContext(innerCtx, "menu:sessions.actions.no_linked_sessions"),
2018
2161
  show_alert: true,
2019
2162
  });
2020
2163
  });
@@ -2052,21 +2195,25 @@ class TelegramTransport {
2052
2195
  userId: ctx.from?.id,
2053
2196
  error: error instanceof Error ? (error.stack ?? error.message) : String(error),
2054
2197
  });
2055
- range.text("⚠ Sessions unavailable", async (innerCtx) => {
2198
+ range.text(await this.tForContext(ctx, "menu:sessions.labels.unavailable"), async (innerCtx) => {
2056
2199
  await innerCtx.answerCallbackQuery({
2057
- text: "Sessions menu is temporarily unavailable.",
2200
+ text: await this.tForContext(innerCtx, "menu:sessions.actions.unavailable"),
2058
2201
  show_alert: true,
2059
2202
  });
2060
2203
  });
2061
2204
  return range;
2062
2205
  }
2063
2206
  })
2064
- .text("🔄 Refresh", async (ctx) => {
2065
- await ctx.answerCallbackQuery({ text: "Sessions refreshed." });
2207
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.refresh"), async (ctx) => {
2208
+ await ctx.answerCallbackQuery({
2209
+ text: await this.tForContext(ctx, "menu:sessions.actions.refreshed"),
2210
+ });
2066
2211
  await this.showSessionsMenu(ctx);
2067
2212
  })
2068
- .text("🛠 Tools", async (ctx) => {
2069
- await ctx.answerCallbackQuery({ text: "Opening tools menu." });
2213
+ .text(async (ctx) => this.tForContext(ctx, "menu:sessions.labels.tools"), async (ctx) => {
2214
+ await ctx.answerCallbackQuery({
2215
+ text: await this.tForContext(ctx, "menu:sessions.actions.open_tools"),
2216
+ });
2070
2217
  await this.showDeveloperMenu(ctx);
2071
2218
  });
2072
2219
  }
@@ -2076,13 +2223,15 @@ class TelegramTransport {
2076
2223
  ...this.createMenuOptions((ctx) => this.showInboxMenu(ctx)),
2077
2224
  })
2078
2225
  .text({
2079
- text: "🗑 Delete",
2226
+ text: async (ctx) => this.tForContext(ctx, "common:menu.delete"),
2080
2227
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2081
2228
  }, async (ctx) => {
2082
2229
  await this.handleInboxMessageDelete(ctx);
2083
2230
  })
2084
- .text("✖ Close", async (ctx) => {
2085
- await ctx.answerCallbackQuery({ text: "Closed." });
2231
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.close"), async (ctx) => {
2232
+ await ctx.answerCallbackQuery({
2233
+ text: await this.tForContext(ctx, "common:menu.close"),
2234
+ });
2086
2235
  await ctx.deleteMessage();
2087
2236
  });
2088
2237
  }
@@ -2092,20 +2241,22 @@ class TelegramTransport {
2092
2241
  ...this.createMenuOptions((ctx) => this.showStorageMenu(ctx)),
2093
2242
  })
2094
2243
  .text({
2095
- text: "📥 Получить",
2244
+ text: async (ctx) => this.tForContext(ctx, "common:menu.get"),
2096
2245
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2097
2246
  }, async (ctx) => {
2098
2247
  await this.handleStorageGet(ctx);
2099
2248
  })
2100
2249
  .text({
2101
- text: "🗑 Delete",
2250
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.delete"),
2102
2251
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2103
2252
  }, async (ctx) => {
2104
2253
  await this.handleStorageDelete(ctx);
2105
2254
  })
2106
2255
  .row()
2107
- .text("⬅ Back", async (ctx) => {
2108
- await ctx.answerCallbackQuery({ text: "Back to storage." });
2256
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
2257
+ await ctx.answerCallbackQuery({
2258
+ text: await this.tForContext(ctx, "menu:storage.actions.back_to_storage"),
2259
+ });
2109
2260
  await this.showStorageMenu(ctx);
2110
2261
  });
2111
2262
  }
@@ -2115,20 +2266,22 @@ class TelegramTransport {
2115
2266
  ...this.createMenuOptions((ctx) => this.showScreenshotsMenu(ctx)),
2116
2267
  })
2117
2268
  .text({
2118
- text: "📥 Получить",
2269
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.get"),
2119
2270
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2120
2271
  }, async (ctx) => {
2121
2272
  await this.handleScreenshotGet(ctx);
2122
2273
  })
2123
2274
  .text({
2124
- text: "🗑 Delete",
2275
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.delete"),
2125
2276
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2126
2277
  }, async (ctx) => {
2127
2278
  await this.handleScreenshotDelete(ctx);
2128
2279
  })
2129
2280
  .row()
2130
- .text("⬅ Back", async (ctx) => {
2131
- await ctx.answerCallbackQuery({ text: "Back to screenshots." });
2281
+ .text(async (ctx) => this.tForContext(ctx, "common:menu.back"), async (ctx) => {
2282
+ await ctx.answerCallbackQuery({
2283
+ text: await this.tForContext(ctx, "menu:screenshots.actions.back_to_screenshots"),
2284
+ });
2132
2285
  await this.showScreenshotsMenu(ctx);
2133
2286
  });
2134
2287
  }
@@ -2474,8 +2627,12 @@ class TelegramTransport {
2474
2627
  });
2475
2628
  }
2476
2629
  await this.replyText(ctx, currentTarget.projectUuid
2477
- ? `Файл отправлен в сессию ${currentTarget.targetSessionLabel}.`
2478
- : `Файл отправлен напарнику ${currentTarget.targetSessionLabel}.`, {
2630
+ ? await this.tForContext(ctx, "menu:handoff.uploaded_to_session", {
2631
+ label: currentTarget.targetSessionLabel,
2632
+ })
2633
+ : await this.tForContext(ctx, "menu:handoff.uploaded_to_partner", {
2634
+ label: currentTarget.targetSessionLabel,
2635
+ }), {
2479
2636
  kind: "inbox",
2480
2637
  sessionId,
2481
2638
  }, { reply_markup: this.mainMenu });
@@ -2499,11 +2656,21 @@ class TelegramTransport {
2499
2656
  });
2500
2657
  await this.replyText(ctx, session?.label
2501
2658
  ? attachments.length === 1
2502
- ? `Файл доставлен в сессию ${session.label}.`
2503
- : `Файлы доставлены в сессию ${session.label}: ${attachments.length}.`
2659
+ ? await this.tForContext(ctx, "menu:handoff.delivered_one", {
2660
+ label: session.label,
2661
+ })
2662
+ : await this.tForContext(ctx, "menu:handoff.delivered_many", {
2663
+ label: session.label,
2664
+ count: attachments.length,
2665
+ })
2504
2666
  : attachments.length === 1
2505
- ? `Файл доставлен в сессию ${sessionId}.`
2506
- : `Файлы доставлены в сессию ${sessionId}: ${attachments.length}.`, {
2667
+ ? await this.tForContext(ctx, "menu:handoff.delivered_one", {
2668
+ label: sessionId,
2669
+ })
2670
+ : await this.tForContext(ctx, "menu:handoff.delivered_many", {
2671
+ label: sessionId,
2672
+ count: attachments.length,
2673
+ }), {
2507
2674
  kind: "inbox",
2508
2675
  sessionId,
2509
2676
  }, { reply_markup: this.mainMenu });
@@ -2685,6 +2852,7 @@ class TelegramTransport {
2685
2852
  const sessionLabel = session.label ?? sessionId;
2686
2853
  const tmuxTarget = session.tmuxTarget ?? "unknown";
2687
2854
  const errorMessage = error instanceof Error ? error.message : String(error);
2855
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
2688
2856
  try {
2689
2857
  await this.sendNotification({
2690
2858
  sessionId,
@@ -2694,11 +2862,17 @@ class TelegramTransport {
2694
2862
  telegramUserId: binding.telegramUserId,
2695
2863
  },
2696
2864
  message: [
2697
- `⚠️ Автоматический tmux nudge для сессии ${sessionLabel} не сработал.`,
2698
- `Сохранённый tmux target больше недействителен: ${tmuxTarget}`,
2699
- `Ошибка: ${errorMessage}`,
2700
- "Обычно это значит, что pane/window/session был пересоздан.",
2701
- "Перепривяжи tmux target для этой сессии.",
2865
+ this.t(locale, "menu:notices.tmux.target_invalid_title", {
2866
+ sessionName: sessionLabel,
2867
+ }),
2868
+ this.t(locale, "menu:notices.tmux.target_invalid_target", {
2869
+ tmuxTarget,
2870
+ }),
2871
+ this.t(locale, "menu:system.error_prefix", {
2872
+ message: errorMessage,
2873
+ }),
2874
+ this.t(locale, "menu:system.tmux_recreated_hint"),
2875
+ this.t(locale, "menu:notices.tmux.target_invalid_action"),
2702
2876
  ].join("\n"),
2703
2877
  });
2704
2878
  }
@@ -2729,6 +2903,7 @@ class TelegramTransport {
2729
2903
  const sessionLabel = session.label ?? sessionId;
2730
2904
  const tmuxTarget = session.tmuxTarget ?? "unknown";
2731
2905
  const errorMessage = error instanceof Error ? error.message : String(error);
2906
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
2732
2907
  try {
2733
2908
  await this.sendNotification({
2734
2909
  sessionId,
@@ -2738,12 +2913,18 @@ class TelegramTransport {
2738
2913
  telegramUserId: binding.telegramUserId,
2739
2914
  },
2740
2915
  message: [
2741
- `⚠️ Автоматический tmux nudge для сессии ${sessionLabel} пропущен.`,
2742
- "tmux сейчас недоступен на этой машине.",
2743
- `tmux target: ${tmuxTarget}`,
2744
- `Ошибка: ${errorMessage}`,
2745
- "Обычно это значит, что tmux session/server не запущен или недоступен по текущему socket path.",
2746
- "Запусти tmux и агента внутри него, либо обнови/сними tmux target для этой сессии.",
2916
+ this.t(locale, "menu:notices.tmux.unavailable_title", {
2917
+ sessionName: sessionLabel,
2918
+ }),
2919
+ this.t(locale, "menu:notices.tmux.unavailable_body"),
2920
+ this.t(locale, "menu:notices.tmux.unavailable_target", {
2921
+ tmuxTarget,
2922
+ }),
2923
+ this.t(locale, "menu:system.error_prefix", {
2924
+ message: errorMessage,
2925
+ }),
2926
+ this.t(locale, "menu:notices.tmux.unavailable_reason"),
2927
+ this.t(locale, "menu:notices.tmux.unavailable_action"),
2747
2928
  ].join("\n"),
2748
2929
  });
2749
2930
  }
@@ -2803,13 +2984,14 @@ class TelegramTransport {
2803
2984
  await this.renderMenuHtmlScreen(ctx, intro ? `${intro}\n\n${text}` : text, { kind: "menu" }, this.mainMenu);
2804
2985
  }
2805
2986
  async buildMainMenuText(ctx) {
2987
+ const locale = await this.resolveLocaleForContext(ctx);
2806
2988
  const principal = this.getPrincipalFromContext(ctx);
2807
2989
  if (!principal) {
2808
- return "Telegram identity is unavailable for this chat.";
2990
+ return this.t(locale, "common:errors.no_telegram_identity");
2809
2991
  }
2810
2992
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2811
2993
  if (!activeSessionId) {
2812
- return "No active session is linked yet. Pair a session via /start <code>.";
2994
+ return this.t(locale, "common:errors.no_active_session");
2813
2995
  }
2814
2996
  const session = await this.sessionStore.getSession(activeSessionId);
2815
2997
  const inboxCount = await this.inboxStore.countInboxMessages(activeSessionId);
@@ -2821,80 +3003,89 @@ class TelegramTransport {
2821
3003
  ? await this.sessionStore.getSession(session.linkedSessionId)
2822
3004
  : null;
2823
3005
  return [
2824
- `🎛 Session: ${sessionName}`,
3006
+ this.t(locale, "menu:main.screen.title", { sessionName }),
2825
3007
  "",
2826
- `📥 Inbox messages: ${inboxCount}`,
2827
- ...(projectName ? [`📦 Project: <b>${projectName}</b>`] : []),
3008
+ this.t(locale, "menu:main.screen.inbox_messages", { count: inboxCount }),
3009
+ ...(projectName
3010
+ ? [this.t(locale, "menu:main.screen.project", { projectName })]
3011
+ : []),
2828
3012
  ...(session?.linkedSessionId
2829
3013
  ? [
2830
- `🤝 Partner: <b><i>${escapeHtml(linkedSession?.label ?? session.linkedSessionId)}</i></b>`,
3014
+ this.t(locale, "menu:main.screen.partner", {
3015
+ partnerName: escapeHtml(linkedSession?.label ?? session.linkedSessionId),
3016
+ }),
2831
3017
  "",
2832
- "Share API details, what's new, errors, and git changes with your teammate.",
3018
+ this.t(locale, "menu:main.screen.partner_hint"),
2833
3019
  ]
2834
- : ["", "🔗 Link a partner session to coordinate through shared notes and files."]),
3020
+ : ["", this.t(locale, "menu:main.screen.link_hint")]),
2835
3021
  ].join("\n");
2836
3022
  }
2837
- async getTmuxStatusLine() {
2838
- return "🖧 TMUX mode: direct";
3023
+ async getTmuxStatusLine(locale) {
3024
+ return this.t(locale, "menu:main.screen.tmux_mode_direct");
2839
3025
  }
2840
3026
  async buildMainMenuFingerprint(ctx) {
3027
+ const locale = await this.resolveLocaleForContext(ctx);
2841
3028
  const principal = this.getPrincipalFromContext(ctx);
2842
3029
  if (!principal) {
2843
- return "no-principal";
3030
+ return `${locale}:no-principal`;
2844
3031
  }
2845
3032
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2846
3033
  if (!sessionId) {
2847
- return "no-active-session";
3034
+ return `${locale}:no-active-session`;
2848
3035
  }
2849
3036
  const count = await this.inboxStore.countInboxMessages(sessionId);
2850
3037
  const session = await this.sessionStore.getSession(sessionId);
2851
- return `${sessionId}:${count}:${session?.linkedSessionId ?? "none"}`;
3038
+ return `${locale}:${sessionId}:${count}:${session?.linkedSessionId ?? "none"}`;
2852
3039
  }
2853
3040
  async buildInboxFingerprint(ctx) {
3041
+ const locale = await this.resolveLocaleForContext(ctx);
2854
3042
  const principal = this.getPrincipalFromContext(ctx);
2855
3043
  if (!principal) {
2856
- return "no-principal";
3044
+ return `${locale}:no-principal`;
2857
3045
  }
2858
3046
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2859
3047
  if (!sessionId) {
2860
- return "no-active-session";
3048
+ return `${locale}:no-active-session`;
2861
3049
  }
2862
3050
  const messages = await this.inboxStore.listInboxMessages(sessionId, 10);
2863
- return `${sessionId}:${messages.map((message) => message.id).join(",")}`;
3051
+ return `${locale}:${sessionId}:${messages.map((message) => message.id).join(",")}`;
2864
3052
  }
2865
3053
  async buildStorageFingerprint(ctx) {
3054
+ const locale = await this.resolveLocaleForContext(ctx);
2866
3055
  const principal = this.getPrincipalFromContext(ctx);
2867
3056
  if (!principal) {
2868
- return "no-principal";
3057
+ return `${locale}:no-principal`;
2869
3058
  }
2870
3059
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2871
3060
  if (!sessionId) {
2872
- return "no-active-session";
3061
+ return `${locale}:no-active-session`;
2873
3062
  }
2874
3063
  const entries = await this.listActiveSessionStorageEntries(sessionId);
2875
- return `${sessionId}:${entries.map((entry) => entry.filePath).join(",")}`;
3064
+ return `${locale}:${sessionId}:${entries.map((entry) => entry.filePath).join(",")}`;
2876
3065
  }
2877
3066
  async buildScreenshotsFingerprint(ctx) {
3067
+ const locale = await this.resolveLocaleForContext(ctx);
2878
3068
  const principal = this.getPrincipalFromContext(ctx);
2879
3069
  if (!principal) {
2880
- return "no-principal";
3070
+ return `${locale}:no-principal`;
2881
3071
  }
2882
3072
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2883
3073
  if (!sessionId) {
2884
- return "no-active-session";
3074
+ return `${locale}:no-active-session`;
2885
3075
  }
2886
3076
  const files = await this.listActiveSessionScreenshots(sessionId);
2887
- return `${sessionId}:${files.join(",")}`;
3077
+ return `${locale}:${sessionId}:${files.join(",")}`;
2888
3078
  }
2889
3079
  async buildSessionsFingerprint(ctx) {
2890
3080
  try {
3081
+ const locale = await this.resolveLocaleForContext(ctx);
2891
3082
  const principal = this.getPrincipalFromContext(ctx);
2892
3083
  if (!principal) {
2893
- return "no-principal";
3084
+ return `${locale}:no-principal`;
2894
3085
  }
2895
3086
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2896
3087
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
2897
- return `${activeSessionId ?? "none"}:${sessionIds.join(",")}`;
3088
+ return `${locale}:${activeSessionId ?? "none"}:${sessionIds.join(",")}`;
2898
3089
  }
2899
3090
  catch (error) {
2900
3091
  this.logger.warn("Failed to build Telegram sessions menu fingerprint", {
@@ -2906,61 +3097,71 @@ class TelegramTransport {
2906
3097
  }
2907
3098
  }
2908
3099
  async buildLinkFingerprint(ctx) {
3100
+ const locale = await this.resolveLocaleForContext(ctx);
2909
3101
  const principal = this.getPrincipalFromContext(ctx);
2910
3102
  if (!principal) {
2911
- return "no-principal";
3103
+ return `${locale}:no-principal`;
2912
3104
  }
2913
3105
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2914
3106
  if (!activeSessionId) {
2915
- return "no-active-session";
3107
+ return `${locale}:no-active-session`;
2916
3108
  }
2917
3109
  const session = await this.sessionStore.getSession(activeSessionId);
2918
3110
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal))
2919
3111
  .filter((sessionId) => sessionId !== activeSessionId)
2920
3112
  .sort();
2921
- return `${activeSessionId}:${session?.linkedSessionId ?? "none"}:${sessionIds.join(",")}`;
3113
+ return `${locale}:${activeSessionId}:${session?.linkedSessionId ?? "none"}:${sessionIds.join(",")}`;
2922
3114
  }
2923
3115
  async buildInboxButtonLabel(ctx) {
3116
+ const locale = await this.resolveLocaleForContext(ctx);
2924
3117
  const principal = this.getPrincipalFromContext(ctx);
2925
3118
  if (!principal) {
2926
- return "Inbox";
3119
+ return this.t(locale, "menu:inbox.button");
2927
3120
  }
2928
3121
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2929
3122
  if (!sessionId) {
2930
- return "📥 Inbox";
3123
+ return this.t(locale, "menu:inbox.button");
2931
3124
  }
2932
3125
  const count = await this.inboxStore.countInboxMessages(sessionId);
2933
- return count > 0 ? `📥 Inbox (${count})` : "📥 Inbox";
3126
+ return count > 0
3127
+ ? this.t(locale, "menu:inbox.button_count", { count })
3128
+ : this.t(locale, "menu:inbox.button");
2934
3129
  }
2935
3130
  async buildScreenshotsButtonLabel(ctx) {
3131
+ const locale = await this.resolveLocaleForContext(ctx);
2936
3132
  const principal = this.getPrincipalFromContext(ctx);
2937
3133
  if (!principal) {
2938
- return "📸 Screenshots";
3134
+ return this.t(locale, "menu:browser.buttons.screenshots");
2939
3135
  }
2940
3136
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2941
3137
  if (!sessionId) {
2942
- return "📸 Screenshots";
3138
+ return this.t(locale, "menu:browser.buttons.screenshots");
2943
3139
  }
2944
3140
  const count = (await this.listActiveSessionScreenshots(sessionId)).length;
2945
- return count > 0 ? `📸 Screenshots (${count})` : "📸 Screenshots";
3141
+ return count > 0
3142
+ ? this.t(locale, "menu:browser.buttons.screenshots_count", { count })
3143
+ : this.t(locale, "menu:browser.buttons.screenshots");
2946
3144
  }
2947
3145
  async buildLinkButtonLabel(ctx) {
3146
+ const locale = await this.resolveLocaleForContext(ctx);
2948
3147
  const principal = this.getPrincipalFromContext(ctx);
2949
3148
  if (!principal) {
2950
- return "🔗 Связать";
3149
+ return this.t(locale, "menu:local.buttons.link");
2951
3150
  }
2952
3151
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2953
3152
  if (!sessionId) {
2954
- return "🔗 Связать";
3153
+ return this.t(locale, "menu:local.buttons.link");
2955
3154
  }
2956
3155
  const session = await this.sessionStore.getSession(sessionId);
2957
3156
  if (!session?.linkedSessionId) {
2958
- return "🔗 Связать";
3157
+ return this.t(locale, "menu:local.buttons.link");
2959
3158
  }
2960
3159
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
2961
3160
  return linkedSession?.label
2962
- ? `🔓 Разорвать ${linkedSession.label}`
2963
- : "🔓 Разорвать";
3161
+ ? this.t(locale, "menu:link.buttons.unlink_with_name", {
3162
+ sessionName: linkedSession.label,
3163
+ })
3164
+ : this.t(locale, "menu:link.buttons.unlink");
2964
3165
  }
2965
3166
  async createInboxMenuPayload(sessionId, messageId) {
2966
3167
  const key = (0, ids_1.createMenuPayloadKey)();
@@ -3198,20 +3399,28 @@ class TelegramTransport {
3198
3399
  (meta?.relativePath ? node_path_1.default.basename(meta.relativePath) : undefined) ||
3199
3400
  node_path_1.default.basename(input.filePath);
3200
3401
  const principalKey = buildPrincipalKey(principal);
3402
+ const locale = await this.resolveLocaleForContext(ctx);
3201
3403
  await ctx.answerCallbackQuery({
3202
- text: "Project file handoff.",
3404
+ text: this.t(locale, "menu:handoff.prompt_title"),
3203
3405
  });
3204
3406
  const sent = await this.replyText(ctx, [
3205
- "🤝 Передать участнику",
3407
+ this.t(locale, "menu:handoff.prompt_title"),
3206
3408
  "",
3207
- `Сессия: ${session?.label ?? input.sessionId} -> ${input.targetSessionLabel}`,
3208
- `Кому: ${input.targetSessionLabel}`,
3209
- `Файл: ${fileName}`,
3409
+ this.t(locale, "menu:handoff.route", {
3410
+ sourceSessionName: session?.label ?? input.sessionId,
3411
+ targetSessionName: input.targetSessionLabel,
3412
+ }),
3413
+ this.t(locale, "menu:handoff.recipient", {
3414
+ label: input.targetSessionLabel,
3415
+ }),
3416
+ this.t(locale, "menu:handoff.file", {
3417
+ fileName,
3418
+ }),
3210
3419
  "",
3211
- "Отправь следующим сообщением описание или инструкции для этого файла.",
3212
- "Этот текст будет приложен к handoff.",
3420
+ this.t(locale, "menu:handoff.prompt_body"),
3421
+ this.t(locale, "menu:handoff.prompt_hint"),
3213
3422
  ].join("\n"), { kind: "menu", sessionId: input.sessionId }, {
3214
- reply_markup: new grammy_1.InlineKeyboard().text("Отмена", "file-handoff-cancel"),
3423
+ reply_markup: new grammy_1.InlineKeyboard().text(this.t(locale, "menu:handoff.cancel"), "file-handoff-cancel"),
3215
3424
  });
3216
3425
  this.pendingFileHandoffs.set(principalKey, {
3217
3426
  sessionId: input.sessionId,
@@ -3225,10 +3434,11 @@ class TelegramTransport {
3225
3434
  });
3226
3435
  }
3227
3436
  async handleLinkButton(ctx) {
3437
+ const locale = await this.resolveLocaleForContext(ctx);
3228
3438
  const principal = this.getPrincipalFromContext(ctx);
3229
3439
  if (!principal) {
3230
3440
  await ctx.answerCallbackQuery({
3231
- text: "Telegram identity недоступна.",
3441
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3232
3442
  show_alert: true,
3233
3443
  });
3234
3444
  return;
@@ -3236,7 +3446,7 @@ class TelegramTransport {
3236
3446
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3237
3447
  if (!sessionId) {
3238
3448
  await ctx.answerCallbackQuery({
3239
- text: "Активная сессия не выбрана.",
3449
+ text: await this.tForContext(ctx, "common:errors.no_active_session"),
3240
3450
  show_alert: true,
3241
3451
  });
3242
3452
  return;
@@ -3244,18 +3454,23 @@ class TelegramTransport {
3244
3454
  const session = await this.sessionStore.getSession(sessionId);
3245
3455
  if (session?.linkedSessionId) {
3246
3456
  await this.unlinkSessions(sessionId, session.linkedSessionId);
3247
- await ctx.answerCallbackQuery({ text: "Partner session unlinked." });
3248
- await this.showMainMenu(ctx, "Partner session unlinked.");
3457
+ await ctx.answerCallbackQuery({
3458
+ text: this.t(locale, "menu:link.actions.unlinked"),
3459
+ });
3460
+ await this.showMainMenu(ctx, this.t(locale, "menu:link.actions.unlinked"));
3249
3461
  return;
3250
3462
  }
3251
- await ctx.answerCallbackQuery({ text: "Choose a partner session." });
3463
+ await ctx.answerCallbackQuery({
3464
+ text: this.t(locale, "menu:link.actions.choose_partner"),
3465
+ });
3252
3466
  await this.showLinkMenu(ctx);
3253
3467
  }
3254
3468
  async showPartnerEntryPoint(ctx) {
3469
+ const locale = await this.resolveLocaleForContext(ctx);
3255
3470
  const principal = this.getPrincipalFromContext(ctx);
3256
3471
  if (!principal) {
3257
3472
  await ctx.answerCallbackQuery({
3258
- text: "Идентификатор Telegram недоступен.",
3473
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3259
3474
  show_alert: true,
3260
3475
  });
3261
3476
  return;
@@ -3263,7 +3478,7 @@ class TelegramTransport {
3263
3478
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3264
3479
  if (!sessionId) {
3265
3480
  await ctx.answerCallbackQuery({
3266
- text: "Активная сессия не выбрана.",
3481
+ text: this.t(locale, "common:errors.no_active_session"),
3267
3482
  show_alert: true,
3268
3483
  });
3269
3484
  return;
@@ -3271,19 +3486,22 @@ class TelegramTransport {
3271
3486
  const session = await this.sessionStore.getSession(sessionId);
3272
3487
  if (!session?.linkedSessionId) {
3273
3488
  await ctx.answerCallbackQuery({
3274
- text: "Сначала свяжи сессию с напарником.",
3489
+ text: await this.tForContext(ctx, "menu:partner.screen.use_link_first"),
3275
3490
  show_alert: true,
3276
3491
  });
3277
3492
  return;
3278
3493
  }
3279
- await ctx.answerCallbackQuery({ text: "Открываю меню напарника." });
3494
+ await ctx.answerCallbackQuery({
3495
+ text: await this.tForContext(ctx, "menu:partner.actions.open_partner_menu"),
3496
+ });
3280
3497
  await this.showPartnerMenu(ctx);
3281
3498
  }
3282
3499
  async showPartnerFiles(ctx) {
3500
+ const locale = await this.resolveLocaleForContext(ctx);
3283
3501
  const principal = this.getPrincipalFromContext(ctx);
3284
3502
  if (!principal) {
3285
3503
  await ctx.answerCallbackQuery({
3286
- text: "Идентификатор Telegram недоступен.",
3504
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3287
3505
  show_alert: true,
3288
3506
  });
3289
3507
  return;
@@ -3291,7 +3509,7 @@ class TelegramTransport {
3291
3509
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3292
3510
  if (!sessionId) {
3293
3511
  await ctx.answerCallbackQuery({
3294
- text: "Активная сессия не выбрана.",
3512
+ text: this.t(locale, "common:errors.no_active_session"),
3295
3513
  show_alert: true,
3296
3514
  });
3297
3515
  return;
@@ -3299,7 +3517,7 @@ class TelegramTransport {
3299
3517
  const session = await this.sessionStore.getSession(sessionId);
3300
3518
  if (!session?.linkedSessionId) {
3301
3519
  await ctx.answerCallbackQuery({
3302
- text: "Сначала свяжи сессию с напарником.",
3520
+ text: this.t(locale, "menu:partner.screen.use_link_first"),
3303
3521
  show_alert: true,
3304
3522
  });
3305
3523
  return;
@@ -3307,13 +3525,15 @@ class TelegramTransport {
3307
3525
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
3308
3526
  const files = await this.listActiveSessionFiles(sessionId);
3309
3527
  const lines = [
3310
- "📎 Выбор файла",
3528
+ this.t(locale, "menu:handoff.choose_title"),
3311
3529
  "",
3312
- `Получатель: ${linkedSession?.label ?? session.linkedSessionId}`,
3530
+ this.t(locale, "menu:handoff.choose_recipient", {
3531
+ label: linkedSession?.label ?? session.linkedSessionId,
3532
+ }),
3313
3533
  "",
3314
3534
  files.length > 0
3315
- ? "Выбери файл для отправки локальному напарнику."
3316
- : этой сессии нет загруженных файлов.",
3535
+ ? this.t(locale, "menu:handoff.choose_local")
3536
+ : this.t(locale, "menu:handoff.no_files"),
3317
3537
  ];
3318
3538
  const keyboard = new grammy_1.InlineKeyboard();
3319
3539
  for (const filePath of files) {
@@ -3322,7 +3542,7 @@ class TelegramTransport {
3322
3542
  const payloadKey = await this.createPartnerFileTargetPayload(sessionId, session.linkedSessionId, linkedSession?.label ?? session.linkedSessionId, filePath);
3323
3543
  keyboard.text(label, `partner-file-open:${payloadKey}`).row();
3324
3544
  }
3325
- keyboard.text("⬅ К напарнику", "partner-back");
3545
+ keyboard.text(await this.tForContext(ctx, "common:menu.back"), "partner-back");
3326
3546
  const text = lines.join("\n");
3327
3547
  if (ctx.callbackQuery?.message) {
3328
3548
  await this.editText(ctx, text, { kind: "menu", sessionId }, { reply_markup: keyboard });
@@ -3331,18 +3551,22 @@ class TelegramTransport {
3331
3551
  await this.replyText(ctx, text, { kind: "menu", sessionId }, { reply_markup: keyboard });
3332
3552
  }
3333
3553
  async showLocalEntryPoint(ctx) {
3334
- await ctx.answerCallbackQuery({ text: "Открываю локальное взаимодействие." });
3554
+ await ctx.answerCallbackQuery({
3555
+ text: await this.tForContext(ctx, "menu:local.actions.open_local"),
3556
+ });
3335
3557
  await this.showLocalMenu(ctx);
3336
3558
  }
3337
3559
  async showProjectsEntryPoint(ctx) {
3338
3560
  if (!this.config.distributed.gatewayPublicUrl) {
3339
3561
  await ctx.answerCallbackQuery({
3340
- text: "Collab доступен только через gateway.",
3562
+ text: await this.tForContext(ctx, "menu:collab.actions.gateway_only"),
3341
3563
  show_alert: true,
3342
3564
  });
3343
3565
  return;
3344
3566
  }
3345
- await ctx.answerCallbackQuery({ text: "Открываю Collab." });
3567
+ await ctx.answerCallbackQuery({
3568
+ text: await this.tForContext(ctx, "menu:collab.actions.open_collab"),
3569
+ });
3346
3570
  await this.showProjectsMenu(ctx);
3347
3571
  }
3348
3572
  async handleLinkTargetSelect(ctx) {
@@ -3882,6 +4106,47 @@ class TelegramTransport {
3882
4106
  telegramUserId: userId,
3883
4107
  };
3884
4108
  }
4109
+ async resolveLocaleForContext(ctx) {
4110
+ if (this.config?.telegram?.debugLanguage) {
4111
+ return (0, i18n_1.normalizeLocale)(this.config.telegram.debugLanguage);
4112
+ }
4113
+ const telegramUserId = ctx.from?.id;
4114
+ const telegramLanguageCode = ctx.from?.language_code;
4115
+ if (!telegramUserId) {
4116
+ return (0, i18n_1.normalizeLocale)(telegramLanguageCode);
4117
+ }
4118
+ const storedLocale = await this.localeStore?.getUserLocale?.(telegramUserId);
4119
+ if (storedLocale) {
4120
+ return (0, i18n_1.normalizeLocale)(storedLocale);
4121
+ }
4122
+ const detectedLocale = (0, i18n_1.normalizeLocale)(telegramLanguageCode);
4123
+ await this.localeStore?.setUserLocale?.(telegramUserId, detectedLocale);
4124
+ return detectedLocale;
4125
+ }
4126
+ async resolveLocaleForTelegramUserId(telegramUserId, telegramLanguageCode) {
4127
+ if (this.config?.telegram?.debugLanguage) {
4128
+ return (0, i18n_1.normalizeLocale)(this.config.telegram.debugLanguage);
4129
+ }
4130
+ if (!telegramUserId) {
4131
+ return (0, i18n_1.normalizeLocale)(telegramLanguageCode);
4132
+ }
4133
+ const storedLocale = await this.localeStore?.getUserLocale?.(telegramUserId);
4134
+ if (storedLocale) {
4135
+ return (0, i18n_1.normalizeLocale)(storedLocale);
4136
+ }
4137
+ const detectedLocale = (0, i18n_1.normalizeLocale)(telegramLanguageCode);
4138
+ await this.localeStore?.setUserLocale?.(telegramUserId, detectedLocale);
4139
+ return detectedLocale;
4140
+ }
4141
+ async tForContext(ctx, key, options) {
4142
+ return this.t(await this.resolveLocaleForContext(ctx), key, options);
4143
+ }
4144
+ async tForTelegramUserId(telegramUserId, key, options) {
4145
+ return this.t(await this.resolveLocaleForTelegramUserId(telegramUserId), key, options);
4146
+ }
4147
+ t(locale, key, options) {
4148
+ return (0, i18n_1.translate)(locale, key, options);
4149
+ }
3885
4150
  getGatewayActorFromContext(ctx) {
3886
4151
  const firstName = ctx.from?.first_name?.trim();
3887
4152
  const lastName = ctx.from?.last_name?.trim();
@@ -3910,7 +4175,7 @@ class TelegramTransport {
3910
4175
  userId: ctx.from?.id,
3911
4176
  error: error instanceof Error ? (error.stack ?? error.message) : String(error),
3912
4177
  });
3913
- await this.replyText(ctx, "Sessions menu is temporarily unavailable. Try /menu again.", { kind: "menu" });
4178
+ await this.replyText(ctx, await this.tForContext(ctx, "menu:system.sessions_menu_unavailable"), { kind: "menu" });
3914
4179
  }
3915
4180
  }
3916
4181
  async showInboxMenu(ctx, introText) {
@@ -3970,15 +4235,18 @@ class TelegramTransport {
3970
4235
  await this.renderMenuScreen(ctx, introText ? `${introText}\n\n${text}` : text, { kind: "menu" }, this.collabToolsMenu);
3971
4236
  }
3972
4237
  buildCollabHistoryMarkdown(input) {
4238
+ const locale = input.locale ?? "en";
3973
4239
  const lines = [
3974
- "# Collab History",
4240
+ this.t(locale, "menu:history.title"),
3975
4241
  "",
3976
- `Session: ${input.sessionLabel}`,
4242
+ this.t(locale, "menu:history.session", {
4243
+ sessionName: input.sessionLabel,
4244
+ }),
3977
4245
  `Generated at: ${new Date().toISOString()}`,
3978
4246
  "",
3979
4247
  ];
3980
4248
  if (input.history.length === 0) {
3981
- lines.push("No recent Collab events.");
4249
+ lines.push(this.t(locale, "menu:history.empty"));
3982
4250
  lines.push("");
3983
4251
  return lines.join("\n");
3984
4252
  }
@@ -3990,7 +4258,9 @@ class TelegramTransport {
3990
4258
  lines.push(`- Direction: ${item.direction}`);
3991
4259
  lines.push(`- Route: ${item.from_label} -> ${item.to_label}`);
3992
4260
  if (item.project_name) {
3993
- lines.push(`- Project: ${item.project_name}`);
4261
+ lines.push(this.t(locale, "menu:history.project", {
4262
+ projectName: item.project_name,
4263
+ }));
3994
4264
  }
3995
4265
  if (item.delivery_status) {
3996
4266
  lines.push(`- Status: ${item.delivery_status}`);
@@ -4001,27 +4271,33 @@ class TelegramTransport {
4001
4271
  return lines.join("\n");
4002
4272
  }
4003
4273
  async handleCollabHistoryExport(ctx) {
4274
+ const locale = await this.resolveLocaleForContext(ctx);
4004
4275
  const { principal, session } = await this.loadProjectsContext(ctx);
4005
4276
  if (!this.config.distributed.gatewayPublicUrl || !principal || !session) {
4006
4277
  await ctx.answerCallbackQuery({
4007
- text: "История Collab недоступна для этой сессии.",
4278
+ text: this.t(locale, "menu:collab.screen.unavailable"),
4008
4279
  show_alert: true,
4009
4280
  });
4010
4281
  return;
4011
4282
  }
4012
4283
  const history = await this.listGatewaySessionHistory(principal, session.sessionId);
4013
4284
  const markdown = this.buildCollabHistoryMarkdown({
4285
+ locale,
4014
4286
  sessionLabel: session.label ?? session.sessionId,
4015
4287
  history,
4016
4288
  });
4017
4289
  const fileName = `collab-history-${slugifyFilenamePart(session.label ?? session.sessionId) || "session"}.md`;
4018
4290
  await this.replyDocumentWithRetry(ctx, new grammy_1.InputFile(Buffer.from(markdown, "utf8"), fileName), {
4019
- caption: `Collab history for ${session.label ?? session.sessionId}`,
4291
+ caption: this.t(locale, "menu:history.caption", {
4292
+ sessionName: session.label ?? session.sessionId,
4293
+ }),
4020
4294
  }, {
4021
4295
  kind: "menu",
4022
4296
  sessionId: session.sessionId,
4023
4297
  });
4024
- await ctx.answerCallbackQuery({ text: "История отправлена в Telegram." });
4298
+ await ctx.answerCallbackQuery({
4299
+ text: this.t(locale, "menu:collab.buttons.history"),
4300
+ });
4025
4301
  }
4026
4302
  async showCollabDeleteMenu(ctx, introText) {
4027
4303
  const text = await this.buildCollabDeleteMenuText(ctx);
@@ -4085,24 +4361,26 @@ class TelegramTransport {
4085
4361
  });
4086
4362
  }
4087
4363
  async showHelp(ctx) {
4364
+ const locale = await this.resolveLocaleForContext(ctx);
4088
4365
  await this.replyText(ctx, [
4089
- "❓ Telegram MCP help",
4366
+ this.t(locale, "menu:help.title"),
4090
4367
  "",
4091
- "/menu - open the sessions list",
4092
- "/help - show this help",
4368
+ this.t(locale, "menu:help.menu"),
4369
+ this.t(locale, "menu:help.help"),
4093
4370
  "",
4094
- "How it works:",
4095
- "- choose the active session",
4096
- "- ordinary Telegram messages go to that session inbox",
4097
- "- if a tmux target is configured, the service nudges the agent automatically",
4098
- "- the agent then reads the inbox batch through MCP tools",
4371
+ this.t(locale, "menu:help.how_it_works"),
4372
+ this.t(locale, "menu:help.step_choose"),
4373
+ this.t(locale, "menu:help.step_inbox"),
4374
+ this.t(locale, "menu:help.step_nudge"),
4375
+ this.t(locale, "menu:help.step_tools"),
4099
4376
  ].join("\n"), { kind: "menu" });
4100
4377
  }
4101
4378
  async showLiveViewLauncher(ctx) {
4379
+ const locale = await this.resolveLocaleForContext(ctx);
4102
4380
  const principal = this.getPrincipalFromContext(ctx);
4103
4381
  if (!principal) {
4104
4382
  await ctx.answerCallbackQuery({
4105
- text: "Telegram identity is unavailable.",
4383
+ text: this.t(locale, "menu:live.errors.identity_unavailable"),
4106
4384
  show_alert: true,
4107
4385
  });
4108
4386
  return;
@@ -4110,7 +4388,7 @@ class TelegramTransport {
4110
4388
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4111
4389
  if (!activeSessionId) {
4112
4390
  await ctx.answerCallbackQuery({
4113
- text: "No active session selected.",
4391
+ text: this.t(locale, "menu:live.errors.no_active_session"),
4114
4392
  show_alert: true,
4115
4393
  });
4116
4394
  return;
@@ -4119,7 +4397,7 @@ class TelegramTransport {
4119
4397
  (!this.config.webapp.publicUrl &&
4120
4398
  !this.config.distributed.gatewayPublicUrl)) {
4121
4399
  await ctx.answerCallbackQuery({
4122
- text: "WebApp is not enabled on the server.",
4400
+ text: this.t(locale, "menu:live.errors.webapp_disabled"),
4123
4401
  show_alert: true,
4124
4402
  });
4125
4403
  return;
@@ -4136,7 +4414,7 @@ class TelegramTransport {
4136
4414
  : resolveWebAppPublicBaseUrl(this.config);
4137
4415
  if (!baseUrl) {
4138
4416
  await ctx.answerCallbackQuery({
4139
- text: "WebApp public URL is not configured.",
4417
+ text: this.t(locale, "menu:live.errors.public_url_missing"),
4140
4418
  show_alert: true,
4141
4419
  });
4142
4420
  return;
@@ -4146,13 +4424,21 @@ class TelegramTransport {
4146
4424
  : activeSessionId;
4147
4425
  const url = new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`);
4148
4426
  url.searchParams.set("launchMode", this.config.webapp.launchMode);
4149
- await ctx.answerCallbackQuery({ text: "Открываю Live View." });
4150
- const sent = await this.replyText(ctx, [`🖥 Live: ${session?.label ?? activeSessionId}`, "", "Выбери режим открытия:"].join("\n"), { kind: "menu", sessionId: activeSessionId }, {
4427
+ await ctx.answerCallbackQuery({
4428
+ text: this.t(locale, "menu:live.actions.opening"),
4429
+ });
4430
+ const sent = await this.replyText(ctx, [
4431
+ this.t(locale, "menu:live.screen.launcher_title", {
4432
+ sessionName: session?.label ?? activeSessionId,
4433
+ }),
4434
+ "",
4435
+ this.t(locale, "menu:live.actions.choose_mode"),
4436
+ ].join("\n"), { kind: "menu", sessionId: activeSessionId }, {
4151
4437
  reply_markup: this.buildLiveViewLaunchKeyboard((mode) => {
4152
4438
  const modeUrl = new URL(url.toString());
4153
4439
  modeUrl.searchParams.set("launchMode", mode);
4154
4440
  return modeUrl.toString();
4155
- }),
4441
+ }, locale),
4156
4442
  });
4157
4443
  this.webAppLaunchRegistry.set(principal.telegramUserId, activeSessionId, this.config.webapp.initDataTtlSeconds, {
4158
4444
  telegramChatId: principal.telegramChatId,
@@ -4183,12 +4469,21 @@ class TelegramTransport {
4183
4469
  url.searchParams.set("launchMode", input.launchMode ?? this.config.webapp.launchMode);
4184
4470
  return url.toString();
4185
4471
  }
4186
- buildLiveViewLaunchKeyboard(getUrl) {
4472
+ buildLiveViewLaunchKeyboard(getUrl, locale = "en") {
4187
4473
  const keyboard = new grammy_1.InlineKeyboard();
4188
4474
  const modes = [
4189
- { mode: "fullscreen", label: "Fullscreen" },
4190
- { mode: "expand", label: "Expand" },
4191
- { mode: "default", label: "Default" },
4475
+ {
4476
+ mode: "fullscreen",
4477
+ label: this.t(locale, "menu:live.buttons.fullscreen"),
4478
+ },
4479
+ {
4480
+ mode: "expand",
4481
+ label: this.t(locale, "menu:live.buttons.expand"),
4482
+ },
4483
+ {
4484
+ mode: "default",
4485
+ label: this.t(locale, "menu:live.buttons.default"),
4486
+ },
4192
4487
  ];
4193
4488
  for (const [index, { mode, label }] of modes.entries()) {
4194
4489
  const url = getUrl(mode);
@@ -4227,10 +4522,11 @@ class TelegramTransport {
4227
4522
  this.currentAttachmentTargets.delete(key);
4228
4523
  }
4229
4524
  async sendActiveSessionBuffer(ctx, scope) {
4525
+ const locale = await this.resolveLocaleForContext(ctx);
4230
4526
  const principal = this.getPrincipalFromContext(ctx);
4231
4527
  if (!principal) {
4232
4528
  await ctx.answerCallbackQuery({
4233
- text: "Telegram identity is unavailable.",
4529
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4234
4530
  show_alert: true,
4235
4531
  });
4236
4532
  return;
@@ -4238,7 +4534,7 @@ class TelegramTransport {
4238
4534
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4239
4535
  if (!sessionId) {
4240
4536
  await ctx.answerCallbackQuery({
4241
- text: "No active session selected.",
4537
+ text: this.t(locale, "common:errors.no_active_session"),
4242
4538
  show_alert: true,
4243
4539
  });
4244
4540
  return;
@@ -4289,14 +4585,15 @@ class TelegramTransport {
4289
4585
  }
4290
4586
  }
4291
4587
  async buildSessionsMenuText(ctx) {
4588
+ const locale = await this.resolveLocaleForContext(ctx);
4292
4589
  const principal = this.getPrincipalFromContext(ctx);
4293
4590
  if (!principal) {
4294
- return "Telegram identity is unavailable for this chat.";
4591
+ return this.t(locale, "common:errors.no_telegram_identity");
4295
4592
  }
4296
4593
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4297
4594
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
4298
4595
  if (sessionIds.length === 0) {
4299
- return "No linked sessions found for this Telegram identity.";
4596
+ return this.t(locale, "menu:sessions.screen.no_linked_sessions");
4300
4597
  }
4301
4598
  let lastWorkedSession;
4302
4599
  for (const sessionId of sessionIds) {
@@ -4315,353 +4612,439 @@ class TelegramTransport {
4315
4612
  };
4316
4613
  }
4317
4614
  }
4318
- const lines = ["🗂 Choose active session", ""];
4615
+ const lines = [this.t(locale, "menu:sessions.screen.title"), ""];
4319
4616
  if (lastWorkedSession) {
4320
- lines.push(`🕘 Last worked: <i>${escapeHtml(lastWorkedSession.label ?? lastWorkedSession.sessionId)}</i>`);
4617
+ lines.push(this.t(locale, "menu:sessions.screen.last_worked", {
4618
+ sessionName: escapeHtml(lastWorkedSession.label ?? lastWorkedSession.sessionId),
4619
+ }));
4321
4620
  const formattedUpdatedAt = formatMenuTimestamp(lastWorkedSession.updatedAt);
4322
4621
  if (formattedUpdatedAt) {
4323
- lines.push(`⏱ Updated: <i>${escapeHtml(formattedUpdatedAt)}</i>`);
4622
+ lines.push(this.t(locale, "menu:sessions.screen.updated", {
4623
+ timestamp: escapeHtml(formattedUpdatedAt),
4624
+ }));
4324
4625
  }
4325
4626
  lines.push("");
4326
4627
  }
4327
4628
  if (activeSessionId) {
4328
4629
  const activeSession = await this.sessionStore.getSession(activeSessionId);
4329
- lines.push(`📌 Current active: <b>${escapeHtml(activeSession?.label ?? activeSessionId)}</b>`);
4630
+ lines.push(this.t(locale, "menu:sessions.screen.current_active", {
4631
+ sessionName: escapeHtml(activeSession?.label ?? activeSessionId),
4632
+ }));
4330
4633
  lines.push("");
4331
4634
  }
4332
- lines.push(`<i>${escapeHtml(await this.getTmuxStatusLine())}</i>`);
4635
+ lines.push(`<i>${escapeHtml(await this.getTmuxStatusLine(locale))}</i>`);
4333
4636
  lines.push("");
4334
4637
  return lines.join("\n");
4335
4638
  }
4336
4639
  async buildInboxMenuText(ctx) {
4640
+ const locale = await this.resolveLocaleForContext(ctx);
4337
4641
  const principal = this.getPrincipalFromContext(ctx);
4338
4642
  if (!principal) {
4339
- return "Telegram identity is unavailable for this chat.";
4643
+ return this.t(locale, "common:errors.no_telegram_identity");
4340
4644
  }
4341
4645
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4342
4646
  if (!activeSessionId) {
4343
- return "No active session is linked yet. Pair a session via /start <code>.";
4647
+ return this.t(locale, "common:errors.no_active_session");
4344
4648
  }
4345
4649
  const session = await this.sessionStore.getSession(activeSessionId);
4346
4650
  const total = await this.inboxStore.countInboxMessages(activeSessionId);
4347
4651
  return [
4348
- "📥 Inbox",
4652
+ this.t(locale, "menu:inbox.screen.title"),
4349
4653
  "",
4350
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4351
- `📨 Stored messages: ${total}`,
4654
+ this.t(locale, "menu:inbox.screen.active_session", {
4655
+ sessionName: session?.label ?? activeSessionId,
4656
+ }),
4657
+ this.t(locale, "menu:inbox.screen.stored_messages", {
4658
+ count: total,
4659
+ }),
4352
4660
  "",
4353
4661
  total > 0
4354
- ? "Choose a message below to inspect or delete it."
4355
- : "No stored unsolicited Telegram messages for this session.",
4662
+ ? this.t(locale, "menu:inbox.screen.choose_message")
4663
+ : this.t(locale, "menu:inbox.screen.empty"),
4356
4664
  ].join("\n");
4357
4665
  }
4358
4666
  async buildBufferMenuText(ctx) {
4667
+ const locale = await this.resolveLocaleForContext(ctx);
4359
4668
  const principal = this.getPrincipalFromContext(ctx);
4360
4669
  if (!principal) {
4361
- return "Telegram identity is unavailable for this chat.";
4670
+ return this.t(locale, "common:errors.no_telegram_identity");
4362
4671
  }
4363
4672
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4364
4673
  if (!activeSessionId) {
4365
- return "No active session is linked yet. Pair a session via /start <code>.";
4674
+ return this.t(locale, "common:errors.no_active_session");
4366
4675
  }
4367
4676
  const session = await this.sessionStore.getSession(activeSessionId);
4368
4677
  return [
4369
- "📄 Content",
4678
+ this.t(locale, "menu:buffer.screen.title"),
4370
4679
  "",
4371
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4372
- `🖥 tmux target: ${session?.tmuxTarget ?? "not set"}`,
4680
+ this.t(locale, "menu:buffer.screen.active_session", {
4681
+ sessionName: session?.label ?? activeSessionId,
4682
+ }),
4683
+ this.t(locale, "menu:buffer.screen.tmux_target", {
4684
+ tmuxTarget: session?.tmuxTarget ?? "not set",
4685
+ }),
4373
4686
  "",
4374
- "Choose how much pane history to export as a Markdown file.",
4375
- "Visible is the current pane viewport. Full exports the whole available tmux history.",
4687
+ this.t(locale, "menu:buffer.screen.export_hint"),
4688
+ this.t(locale, "menu:buffer.screen.export_modes"),
4376
4689
  ].join("\n");
4377
4690
  }
4378
4691
  async buildBrowserMenuText(ctx) {
4692
+ const locale = await this.resolveLocaleForContext(ctx);
4379
4693
  const principal = this.getPrincipalFromContext(ctx);
4380
4694
  if (!principal) {
4381
- return "Telegram identity is unavailable for this chat.";
4695
+ return this.t(locale, "common:errors.no_telegram_identity");
4382
4696
  }
4383
4697
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4384
4698
  if (!activeSessionId) {
4385
- return "No active session is linked yet. Pair a session via /start <code>.";
4699
+ return this.t(locale, "common:errors.no_active_session");
4386
4700
  }
4387
4701
  const session = await this.sessionStore.getSession(activeSessionId);
4388
4702
  const screenshots = await this.listActiveSessionScreenshots(activeSessionId);
4389
4703
  return [
4390
- "🌐 Browser",
4704
+ this.t(locale, "menu:browser.screen.title"),
4391
4705
  "",
4392
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4393
- `📸 Stored screenshots: ${screenshots.length}`,
4706
+ this.t(locale, "menu:browser.screen.active_session", {
4707
+ sessionName: session?.label ?? activeSessionId,
4708
+ }),
4709
+ this.t(locale, "menu:browser.screen.stored_screenshots", {
4710
+ count: screenshots.length,
4711
+ }),
4394
4712
  "",
4395
- "Choose a browser-related action below.",
4713
+ this.t(locale, "menu:browser.screen.choose_action"),
4396
4714
  ].join("\n");
4397
4715
  }
4398
4716
  async buildSettingsMenuText(ctx) {
4717
+ const locale = await this.resolveLocaleForContext(ctx);
4399
4718
  const principal = this.getPrincipalFromContext(ctx);
4400
4719
  if (!principal) {
4401
- return "Telegram identity is unavailable for this chat.";
4720
+ return this.t(locale, "common:errors.no_telegram_identity");
4402
4721
  }
4403
4722
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4404
4723
  if (!activeSessionId) {
4405
- return "No active session is linked yet. Pair a session via /start <code>.";
4724
+ return this.t(locale, "common:errors.no_active_session");
4406
4725
  }
4407
4726
  const session = await this.sessionStore.getSession(activeSessionId);
4408
4727
  return [
4409
- "⚙ Settings",
4728
+ this.t(locale, "menu:settings.screen.title"),
4410
4729
  "",
4411
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4730
+ this.t(locale, "menu:settings.screen.active_session", {
4731
+ sessionName: session?.label ?? activeSessionId,
4732
+ }),
4412
4733
  "",
4413
- "Открой информацию о сессии, переименуй её или отвяжи от Telegram.",
4734
+ this.t(locale, "menu:settings.screen.hint"),
4414
4735
  ].join("\n");
4415
4736
  }
4416
4737
  async buildScreenshotsMenuText(ctx) {
4738
+ const locale = await this.resolveLocaleForContext(ctx);
4417
4739
  const principal = this.getPrincipalFromContext(ctx);
4418
4740
  if (!principal) {
4419
- return "Telegram identity is unavailable for this chat.";
4741
+ return this.t(locale, "common:errors.no_telegram_identity");
4420
4742
  }
4421
4743
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4422
4744
  if (!activeSessionId) {
4423
- return "No active session is linked yet. Pair a session via /start <code>.";
4745
+ return this.t(locale, "common:errors.no_active_session");
4424
4746
  }
4425
4747
  const session = await this.sessionStore.getSession(activeSessionId);
4426
4748
  const files = await this.listActiveSessionScreenshots(activeSessionId);
4427
4749
  return [
4428
- "📸 Screenshots",
4750
+ this.t(locale, "menu:screenshots.screen.title"),
4429
4751
  "",
4430
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4431
- `📦 Stored screenshots: ${files.length}`,
4752
+ this.t(locale, "menu:screenshots.screen.active_session", {
4753
+ sessionName: session?.label ?? activeSessionId,
4754
+ }),
4755
+ this.t(locale, "menu:screenshots.screen.stored_screenshots", {
4756
+ count: files.length,
4757
+ }),
4432
4758
  "",
4433
4759
  files.length > 0
4434
- ? "Choose a screenshot below to get it in Telegram or delete it."
4435
- : "No browser screenshots are stored for this session.",
4760
+ ? this.t(locale, "menu:screenshots.screen.choose_screenshot")
4761
+ : this.t(locale, "menu:screenshots.screen.empty"),
4436
4762
  ].join("\n");
4437
4763
  }
4438
4764
  async buildStorageMenuText(ctx) {
4765
+ const locale = await this.resolveLocaleForContext(ctx);
4439
4766
  const principal = this.getPrincipalFromContext(ctx);
4440
4767
  if (!principal) {
4441
- return "Telegram identity is unavailable for this chat.";
4768
+ return this.t(locale, "common:errors.no_telegram_identity");
4442
4769
  }
4443
4770
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4444
4771
  if (!activeSessionId) {
4445
- return "No active session is linked yet. Pair a session via /start <code>.";
4772
+ return this.t(locale, "common:errors.no_active_session");
4446
4773
  }
4447
4774
  const session = await this.sessionStore.getSession(activeSessionId);
4448
4775
  const entries = await this.listActiveSessionStorageEntries(activeSessionId);
4449
4776
  return [
4450
- "📦 Storage",
4777
+ this.t(locale, "menu:storage.screen.title"),
4451
4778
  "",
4452
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4453
- `📦 Stored files: ${entries.length}`,
4779
+ this.t(locale, "menu:storage.screen.active_session", {
4780
+ sessionName: session?.label ?? activeSessionId,
4781
+ }),
4782
+ this.t(locale, "menu:storage.screen.stored_files", {
4783
+ count: entries.length,
4784
+ }),
4454
4785
  "",
4455
4786
  entries.length > 0
4456
- ? "Choose a file below to inspect it or send it to Telegram."
4457
- : "No .mcp-xchange files are stored for this session.",
4787
+ ? this.t(locale, "menu:storage.screen.choose_file")
4788
+ : this.t(locale, "menu:storage.screen.empty"),
4458
4789
  ].join("\n");
4459
4790
  }
4460
4791
  async buildLinkMenuText(ctx) {
4792
+ const locale = await this.resolveLocaleForContext(ctx);
4461
4793
  const principal = this.getPrincipalFromContext(ctx);
4462
4794
  if (!principal) {
4463
- return "Telegram identity is unavailable for this chat.";
4795
+ return this.t(locale, "common:errors.no_telegram_identity");
4464
4796
  }
4465
4797
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4466
4798
  if (!activeSessionId) {
4467
- return "No active session is linked yet. Pair a session via /start <code>.";
4799
+ return this.t(locale, "common:errors.no_active_session");
4468
4800
  }
4469
4801
  const session = await this.sessionStore.getSession(activeSessionId);
4470
4802
  return [
4471
- "🔗 Link partner",
4803
+ this.t(locale, "menu:link.screen.title"),
4472
4804
  "",
4473
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4805
+ this.t(locale, "menu:link.screen.active_session", {
4806
+ sessionName: session?.label ?? activeSessionId,
4807
+ }),
4474
4808
  "",
4475
- "Choose another session to link as a teammate.",
4476
- "Use this partnership to share API summaries, what's new, errors, and relevant git changes through .mcp-xchange notes and files.",
4809
+ this.t(locale, "menu:link.screen.choose_partner"),
4810
+ this.t(locale, "menu:link.screen.hint"),
4477
4811
  ].join("\n");
4478
4812
  }
4479
4813
  async buildPartnerMenuText(ctx) {
4814
+ const locale = await this.resolveLocaleForContext(ctx);
4480
4815
  const principal = this.getPrincipalFromContext(ctx);
4481
4816
  if (!principal) {
4482
- return "Telegram identity is unavailable for this chat.";
4817
+ return this.t(locale, "common:errors.no_telegram_identity");
4483
4818
  }
4484
4819
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4485
4820
  if (!activeSessionId) {
4486
- return "No active session is linked yet. Pair a session via /start <code>.";
4821
+ return this.t(locale, "common:errors.no_active_session");
4487
4822
  }
4488
4823
  const session = await this.sessionStore.getSession(activeSessionId);
4489
4824
  if (!session?.linkedSessionId) {
4490
4825
  return [
4491
- "🤝 Partner",
4826
+ this.t(locale, "menu:partner.screen.title"),
4492
4827
  "",
4493
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4828
+ this.t(locale, "menu:partner.screen.active_session", {
4829
+ sessionName: session?.label ?? activeSessionId,
4830
+ }),
4494
4831
  "",
4495
- "No partner is linked yet.",
4496
- "Use Link in the session menu first.",
4832
+ this.t(locale, "menu:partner.screen.no_partner"),
4833
+ this.t(locale, "menu:partner.screen.use_link_first"),
4497
4834
  ].join("\n");
4498
4835
  }
4499
4836
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
4500
4837
  return [
4501
- "🤝 Partner",
4838
+ this.t(locale, "menu:partner.screen.title"),
4502
4839
  "",
4503
- `📌 Active session: ${session.label ?? activeSessionId}`,
4504
- `👥 Linked partner: ${linkedSession?.label ?? session.linkedSessionId}`,
4840
+ this.t(locale, "menu:partner.screen.active_session", {
4841
+ sessionName: session.label ?? activeSessionId,
4842
+ }),
4843
+ this.t(locale, "menu:partner.screen.linked_partner", {
4844
+ partnerName: linkedSession?.label ?? session.linkedSessionId,
4845
+ }),
4505
4846
  "",
4506
- "Ask for API details or share what changed.",
4507
- "Prompt format: first line is summary. Add a blank line and then the main message body if needed.",
4847
+ this.t(locale, "menu:partner.screen.prompt_hint"),
4848
+ this.t(locale, "menu:partner.screen.prompt_format"),
4508
4849
  ].join("\n");
4509
4850
  }
4510
4851
  async buildLocalMenuText(ctx) {
4852
+ const locale = await this.resolveLocaleForContext(ctx);
4511
4853
  const principal = this.getPrincipalFromContext(ctx);
4512
4854
  if (!principal) {
4513
- return "Локальное взаимодействие недоступно для этого чата.";
4855
+ return this.t(locale, "menu:local.screen.unavailable");
4514
4856
  }
4515
4857
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4516
4858
  if (!activeSessionId) {
4517
- return "Активная сессия не выбрана. Сначала привяжи её через /start.";
4859
+ return this.t(locale, "menu:local.screen.no_active_session");
4518
4860
  }
4519
4861
  const session = await this.sessionStore.getSession(activeSessionId);
4520
4862
  const linkedSession = session?.linkedSessionId
4521
4863
  ? await this.sessionStore.getSession(session.linkedSessionId)
4522
4864
  : null;
4523
4865
  return [
4524
- "🏠 Local",
4866
+ this.t(locale, "menu:main.buttons.local"),
4525
4867
  "",
4526
- `📌 Активная сессия: ${session?.label ?? activeSessionId}`,
4527
- `🤝 Связь: ${linkedSession?.label ?? "не настроена"}`,
4868
+ this.t(locale, "menu:local.screen.active_session", {
4869
+ sessionName: session?.label ?? activeSessionId,
4870
+ }),
4871
+ linkedSession?.label
4872
+ ? this.t(locale, "menu:local.screen.link_status", {
4873
+ linkedSessionName: linkedSession.label,
4874
+ })
4875
+ : this.t(locale, "menu:local.screen.link_status_none"),
4528
4876
  "",
4529
- "Здесь живёт локальная работа в одном боте:",
4530
- "связка сессий, обмен note и файлами без gateway.",
4877
+ this.t(locale, "menu:local.screen.hint_title"),
4878
+ this.t(locale, "menu:local.screen.hint_body"),
4531
4879
  ].join("\n");
4532
4880
  }
4533
4881
  async buildProjectsMenuText(ctx) {
4882
+ const locale = await this.resolveLocaleForContext(ctx);
4534
4883
  const { session, projects } = await this.loadProjectsContext(ctx);
4535
4884
  if (!this.config.distributed.gatewayPublicUrl) {
4536
4885
  return [
4537
- "👥 Collab",
4886
+ this.t(locale, "menu:collab.screen.title"),
4538
4887
  "",
4539
- "Gateway не настроен для этого запуска.",
4540
- "Для локальной работы в одном боте используй раздел Local.",
4888
+ this.t(locale, "menu:collab.screen.gateway_not_configured"),
4889
+ this.t(locale, "menu:collab.screen.use_local_instead"),
4541
4890
  ].join("\n");
4542
4891
  }
4543
4892
  if (!session || !projects) {
4544
- return "Collab недоступен для текущей сессии.";
4893
+ return this.t(locale, "menu:collab.screen.unavailable");
4545
4894
  }
4546
4895
  return [
4547
- "👥 Collab",
4896
+ this.t(locale, "menu:collab.screen.title"),
4548
4897
  "",
4549
- `📌 Активная сессия: ${session.label ?? session.sessionId}`,
4550
- `📦 Открытый проект: ${session.activeProjectName ?? "не выбран"}`,
4551
- `🗂 Доступно проектов: ${projects.length}`,
4898
+ this.t(locale, "menu:collab.screen.active_session", {
4899
+ sessionName: session.label ?? session.sessionId,
4900
+ }),
4901
+ session.activeProjectName
4902
+ ? this.t(locale, "menu:collab.screen.open_project", {
4903
+ projectName: session.activeProjectName,
4904
+ })
4905
+ : this.t(locale, "menu:collab.screen.open_project_none"),
4906
+ this.t(locale, "menu:collab.screen.project_count", {
4907
+ count: projects.length,
4908
+ }),
4552
4909
  "",
4553
- "Открой проект, создай новый или войди по invite-коду.",
4910
+ this.t(locale, "menu:collab.screen.invite_hint"),
4554
4911
  ].join("\n");
4555
4912
  }
4556
4913
  async buildCollabToolsMenuText(ctx) {
4914
+ const locale = await this.resolveLocaleForContext(ctx);
4557
4915
  const principal = this.getPrincipalFromContext(ctx);
4558
4916
  if (!principal || !this.config.distributed.gatewayPublicUrl) {
4559
- return "Collab tools недоступны для этого запуска.";
4917
+ return this.t(locale, "menu:collab.screen.gateway_not_configured");
4560
4918
  }
4561
4919
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4562
4920
  if (!sessionId) {
4563
- return "Активная сессия не выбрана.";
4921
+ return this.t(locale, "common:errors.no_active_session");
4564
4922
  }
4565
4923
  const session = await this.sessionStore.getSession(sessionId);
4566
4924
  const projects = await this.listGatewayProjects(principal);
4567
4925
  if (projects.length === 0) {
4568
4926
  return [
4569
- "🛠 Collab Tools",
4927
+ this.t(locale, "menu:collab.screen.tools_title"),
4570
4928
  "",
4571
- `📌 Активная сессия: ${session?.label ?? sessionId}`,
4929
+ this.t(locale, "menu:collab.screen.active_session", {
4930
+ sessionName: session?.label ?? sessionId,
4931
+ }),
4572
4932
  "",
4573
- "Сначала создай проект или войди в существующий.",
4933
+ this.t(locale, "menu:collab.screen.tools_empty"),
4574
4934
  ].join("\n");
4575
4935
  }
4576
4936
  const targets = await this.collectCollabBroadcastTargets(principal, sessionId);
4577
4937
  const uniqueCount = targets.localTargetSessionIds.length + targets.remoteTargets.length;
4578
4938
  return [
4579
- "🛠 Collab Tools",
4939
+ this.t(locale, "menu:collab.screen.tools_title"),
4580
4940
  "",
4581
- `📌 Активная сессия: ${session?.label ?? sessionId}`,
4582
- `🗂 Проектов в Collab: ${projects.length}`,
4583
- `👥 Уникальных сессий: ${uniqueCount}`,
4941
+ this.t(locale, "menu:collab.screen.active_session", {
4942
+ sessionName: session?.label ?? sessionId,
4943
+ }),
4944
+ this.t(locale, "menu:collab.screen.tools_project_count", {
4945
+ count: projects.length,
4946
+ }),
4947
+ this.t(locale, "menu:collab.screen.tools_session_count", {
4948
+ count: uniqueCount,
4949
+ }),
4584
4950
  "",
4585
- "Broadcast отправит следующее текстовое сообщение всем уникальным Collab-сессиям на ботах без дублирования.",
4586
- "History отправит .md с последними 5 Collab-событиями текущей сессии.",
4587
- "Локальные сессии получат inbox message, удалённые — gateway delivery.",
4951
+ this.t(locale, "menu:collab.screen.tools_broadcast"),
4952
+ this.t(locale, "menu:collab.screen.tools_history"),
4953
+ this.t(locale, "menu:broadcast.collab_hint"),
4588
4954
  ].join("\n");
4589
4955
  }
4590
4956
  async buildCollabDeleteMenuText(ctx) {
4957
+ const locale = await this.resolveLocaleForContext(ctx);
4591
4958
  const { session, projects } = await this.loadProjectsContext(ctx);
4592
4959
  if (!this.config.distributed.gatewayPublicUrl) {
4593
- return "Удаление проектов недоступно для этого запуска.";
4960
+ return this.t(locale, "menu:collab.screen.gateway_not_configured");
4594
4961
  }
4595
4962
  if (!session || !projects) {
4596
- return "Удаление проектов недоступно для текущей сессии.";
4963
+ return this.t(locale, "menu:collab.screen.unavailable");
4597
4964
  }
4598
4965
  const ownerCount = projects.filter((project) => project.role === "owner").length;
4599
4966
  return [
4600
- "🗑 Delete Project",
4967
+ this.t(locale, "menu:project.delete_menu_title"),
4601
4968
  "",
4602
- `📌 Активная сессия: ${session.label ?? session.sessionId}`,
4603
- `🗂 Проектов в Collab: ${projects.length}`,
4604
- `👑 Проектов, где ты owner: ${ownerCount}`,
4969
+ this.t(locale, "menu:project.active_session", {
4970
+ sessionName: session.label ?? session.sessionId,
4971
+ }),
4972
+ this.t(locale, "menu:project.total_count", {
4973
+ count: projects.length,
4974
+ }),
4975
+ this.t(locale, "menu:project.owner_count", {
4976
+ count: ownerCount,
4977
+ }),
4605
4978
  "",
4606
- "Выбери проект для полного удаления.",
4607
- "Будут отвязаны участники, удалены project sessions и сама запись проекта.",
4608
- "Удалять проект может только owner.",
4979
+ this.t(locale, "menu:project.delete_choose"),
4980
+ this.t(locale, "menu:project.delete_body"),
4981
+ this.t(locale, "menu:project.delete_owner_hint"),
4609
4982
  ].join("\n");
4610
4983
  }
4611
4984
  async buildDeveloperMenuText(ctx) {
4985
+ const locale = await this.resolveLocaleForContext(ctx);
4612
4986
  const principal = this.getPrincipalFromContext(ctx);
4613
4987
  if (!principal) {
4614
- return "Telegram identity is unavailable for this chat.";
4988
+ return this.t(locale, "common:errors.no_telegram_identity");
4615
4989
  }
4616
4990
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4617
4991
  return [
4618
- "🛠 Tools",
4992
+ this.t(locale, "menu:developer.screen.title"),
4619
4993
  "",
4620
- `🔗 Linked sessions: ${sessionIds.length}`,
4994
+ this.t(locale, "menu:developer.screen.linked_sessions", {
4995
+ count: sessionIds.length,
4996
+ }),
4621
4997
  "",
4622
- "Broadcast writes your next text message into every linked session inbox and nudges all configured tmux targets.",
4623
- "Prune all clears every Redis key under this Telegram MCP namespace.",
4998
+ this.t(locale, "menu:developer.screen.broadcast_help"),
4999
+ this.t(locale, "menu:developer.screen.prune_help"),
4624
5000
  ].join("\n");
4625
5001
  }
4626
5002
  async buildUnpairConfirmText(ctx) {
5003
+ const locale = await this.resolveLocaleForContext(ctx);
4627
5004
  const principal = this.getPrincipalFromContext(ctx);
4628
5005
  if (!principal) {
4629
- return "Telegram identity is unavailable for this chat.";
5006
+ return this.t(locale, "common:errors.no_telegram_identity");
4630
5007
  }
4631
5008
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4632
5009
  if (!activeSessionId) {
4633
- return "No active session selected.";
5010
+ return this.t(locale, "common:errors.no_active_session");
4634
5011
  }
4635
5012
  const session = await this.sessionStore.getSession(activeSessionId);
4636
5013
  return [
4637
- "⚠ Confirm unpair",
5014
+ this.t(locale, "menu:unpair.title"),
4638
5015
  "",
4639
- `📌 Active session: ${session?.label ?? activeSessionId}`,
5016
+ this.t(locale, "menu:unpair.active_session", {
5017
+ sessionName: session?.label ?? activeSessionId,
5018
+ }),
4640
5019
  "",
4641
- "This removes the Telegram binding for the active session.",
4642
- "Session metadata and inbox records stay in Redis until you delete them separately.",
5020
+ this.t(locale, "menu:unpair.body_1"),
5021
+ this.t(locale, "menu:unpair.body_2"),
4643
5022
  ].join("\n");
4644
5023
  }
4645
5024
  async buildPruneConfirmText(ctx) {
5025
+ const locale = await this.resolveLocaleForContext(ctx);
4646
5026
  const principal = this.getPrincipalFromContext(ctx);
4647
5027
  if (!principal) {
4648
- return "Telegram identity is unavailable for this chat.";
5028
+ return this.t(locale, "common:errors.no_telegram_identity");
4649
5029
  }
4650
5030
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4651
5031
  return [
4652
- "⚠ Confirm prune",
5032
+ this.t(locale, "menu:prune.title"),
4653
5033
  "",
4654
- `🔗 Linked sessions visible here: ${sessionIds.length}`,
5034
+ this.t(locale, "menu:prune.linked_sessions", {
5035
+ count: sessionIds.length,
5036
+ }),
4655
5037
  "",
4656
- "This clears every Redis key under the telegram-mcp namespace.",
4657
- "Pair codes, bindings, sessions, inbox, menu payloads, and pending requests will all be deleted.",
5038
+ this.t(locale, "menu:prune.body_1"),
5039
+ this.t(locale, "menu:prune.body_2"),
4658
5040
  ].join("\n");
4659
5041
  }
4660
5042
  async showActiveSessionInfo(ctx) {
5043
+ const locale = await this.resolveLocaleForContext(ctx);
4661
5044
  const principal = this.getPrincipalFromContext(ctx);
4662
5045
  if (!principal) {
4663
5046
  await ctx.answerCallbackQuery({
4664
- text: "Telegram identity is unavailable.",
5047
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4665
5048
  show_alert: true,
4666
5049
  });
4667
5050
  return;
@@ -4669,7 +5052,7 @@ class TelegramTransport {
4669
5052
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4670
5053
  if (!sessionId) {
4671
5054
  await ctx.answerCallbackQuery({
4672
- text: "No active session selected.",
5055
+ text: this.t(locale, "common:errors.no_active_session"),
4673
5056
  show_alert: true,
4674
5057
  });
4675
5058
  return;
@@ -4680,23 +5063,55 @@ class TelegramTransport {
4680
5063
  const linkedSession = session?.linkedSessionId
4681
5064
  ? await this.sessionStore.getSession(session.linkedSessionId)
4682
5065
  : null;
4683
- await ctx.answerCallbackQuery({ text: "Session info opened." });
5066
+ await ctx.answerCallbackQuery({
5067
+ text: this.t(locale, "menu:session_info.opened"),
5068
+ });
4684
5069
  await this.replyText(ctx, [
4685
- "ℹ Session info",
5070
+ this.t(locale, "menu:session_info.title"),
4686
5071
  "",
4687
- `📌 Label: ${session?.label ?? sessionId}`,
4688
- `🆔 Session ID: ${sessionId}`,
4689
- `📥 Inbox count: ${inboxCount}`,
4690
- `🔗 Paired: ${binding ? "yes" : "no"}`,
4691
- `🤝 Partner: ${linkedSession?.label ?? session?.linkedSessionId ?? "not linked"}`,
4692
- `🖥 tmux target: ${session?.tmuxTarget ?? "not set"}`,
5072
+ this.t(locale, "menu:session_info.label", {
5073
+ value: session?.label ?? sessionId,
5074
+ }),
5075
+ this.t(locale, "menu:session_info.session_id", {
5076
+ value: sessionId,
5077
+ }),
5078
+ this.t(locale, "menu:session_info.inbox_count", {
5079
+ count: inboxCount,
5080
+ }),
5081
+ this.t(locale, "menu:session_info.paired", {
5082
+ value: binding
5083
+ ? this.t(locale, "menu:session_info.yes")
5084
+ : this.t(locale, "menu:session_info.no"),
5085
+ }),
5086
+ this.t(locale, "menu:session_info.partner", {
5087
+ value: linkedSession?.label ??
5088
+ session?.linkedSessionId ??
5089
+ this.t(locale, "menu:session_info.not_linked"),
5090
+ }),
5091
+ this.t(locale, "menu:session_info.tmux_target", {
5092
+ value: session?.tmuxTarget ?? this.t(locale, "menu:session_info.not_set"),
5093
+ }),
4693
5094
  ...(session?.tmuxSessionName
4694
- ? [`📺 tmux session: ${session.tmuxSessionName}`]
5095
+ ? [
5096
+ this.t(locale, "menu:session_info.tmux_session", {
5097
+ value: session.tmuxSessionName,
5098
+ }),
5099
+ ]
4695
5100
  : []),
4696
5101
  ...(session?.tmuxWindowName
4697
- ? [`🪟 tmux window: ${session.tmuxWindowName}`]
5102
+ ? [
5103
+ this.t(locale, "menu:session_info.tmux_window", {
5104
+ value: session.tmuxWindowName,
5105
+ }),
5106
+ ]
5107
+ : []),
5108
+ ...(session?.tmuxPaneId
5109
+ ? [
5110
+ this.t(locale, "menu:session_info.tmux_pane", {
5111
+ value: session.tmuxPaneId,
5112
+ }),
5113
+ ]
4698
5114
  : []),
4699
- ...(session?.tmuxPaneId ? [`🔹 tmux pane: ${session.tmuxPaneId}`] : []),
4700
5115
  ].join("\n"), { kind: "menu", sessionId }, { reply_markup: this.settingsMenu });
4701
5116
  }
4702
5117
  async linkSessions(sessionId, targetSessionId) {
@@ -4837,10 +5252,11 @@ class TelegramTransport {
4837
5252
  }
4838
5253
  }
4839
5254
  async unpairActiveSession(ctx) {
5255
+ const locale = await this.resolveLocaleForContext(ctx);
4840
5256
  const principal = this.getPrincipalFromContext(ctx);
4841
5257
  if (!principal) {
4842
5258
  await ctx.answerCallbackQuery({
4843
- text: "Telegram identity is unavailable.",
5259
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4844
5260
  show_alert: true,
4845
5261
  });
4846
5262
  return;
@@ -4848,7 +5264,7 @@ class TelegramTransport {
4848
5264
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4849
5265
  if (!sessionId) {
4850
5266
  await ctx.answerCallbackQuery({
4851
- text: "No active session selected.",
5267
+ text: this.t(locale, "common:errors.no_active_session"),
4852
5268
  show_alert: true,
4853
5269
  });
4854
5270
  return;
@@ -4877,19 +5293,20 @@ class TelegramTransport {
4877
5293
  });
4878
5294
  this.clearPendingInteractionsForContext(ctx);
4879
5295
  await ctx.answerCallbackQuery({
4880
- text: session?.label
4881
- ? `Unpaired: ${session.label}`
4882
- : `Unpaired: ${sessionId}`,
5296
+ text: this.t(locale, "menu:unpair.done", {
5297
+ sessionName: session?.label ?? sessionId,
5298
+ }),
4883
5299
  });
4884
- await this.showSessionsMenu(ctx, session?.label
4885
- ? `Session unpaired: ${session.label}`
4886
- : `Session unpaired: ${sessionId}`);
5300
+ await this.showSessionsMenu(ctx, this.t(locale, "menu:unpair.shown", {
5301
+ sessionName: session?.label ?? sessionId,
5302
+ }));
4887
5303
  }
4888
5304
  async beginRenameActiveSession(ctx) {
5305
+ const locale = await this.resolveLocaleForContext(ctx);
4889
5306
  const principal = this.getPrincipalFromContext(ctx);
4890
5307
  if (!principal) {
4891
5308
  await ctx.answerCallbackQuery({
4892
- text: "Telegram identity is unavailable.",
5309
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4893
5310
  show_alert: true,
4894
5311
  });
4895
5312
  return;
@@ -4897,7 +5314,7 @@ class TelegramTransport {
4897
5314
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4898
5315
  if (!sessionId) {
4899
5316
  await ctx.answerCallbackQuery({
4900
- text: "No active session selected.",
5317
+ text: this.t(locale, "common:errors.no_active_session"),
4901
5318
  show_alert: true,
4902
5319
  });
4903
5320
  return;
@@ -4905,19 +5322,17 @@ class TelegramTransport {
4905
5322
  const principalKey = buildPrincipalKey(principal);
4906
5323
  this.pendingBroadcasts.delete(principalKey);
4907
5324
  this.pendingRenames.set(principalKey, { sessionId });
4908
- await ctx.answerCallbackQuery({ text: "Send the new session title." });
4909
- await this.replyText(ctx, [
4910
- "✏ Rename session",
4911
- "",
4912
- "Send the next text message as the new title for the active session.",
4913
- "Commands like /menu or /help will cancel rename mode.",
4914
- ].join("\n"), { kind: "menu", sessionId });
5325
+ await ctx.answerCallbackQuery({
5326
+ text: this.t(locale, "menu:settings.actions.rename_prompt"),
5327
+ });
5328
+ await this.replyText(ctx, ["✏ Rename session", "", this.t(locale, "menu:settings.actions.rename_body")].join("\n"), { kind: "menu", sessionId });
4915
5329
  }
4916
5330
  async beginBroadcast(ctx) {
5331
+ const locale = await this.resolveLocaleForContext(ctx);
4917
5332
  const principal = this.getPrincipalFromContext(ctx);
4918
5333
  if (!principal) {
4919
5334
  await ctx.answerCallbackQuery({
4920
- text: "Telegram identity is unavailable.",
5335
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4921
5336
  show_alert: true,
4922
5337
  });
4923
5338
  return;
@@ -4925,7 +5340,7 @@ class TelegramTransport {
4925
5340
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4926
5341
  if (sessionIds.length === 0) {
4927
5342
  await ctx.answerCallbackQuery({
4928
- text: "No linked sessions found.",
5343
+ text: this.t(locale, "menu:broadcast.no_linked_sessions"),
4929
5344
  show_alert: true,
4930
5345
  });
4931
5346
  return;
@@ -4933,14 +5348,18 @@ class TelegramTransport {
4933
5348
  const principalKey = buildPrincipalKey(principal);
4934
5349
  this.pendingRenames.delete(principalKey);
4935
5350
  await ctx.answerCallbackQuery({
4936
- text: `Broadcast to ${sessionIds.length} sessions.`,
5351
+ text: this.t(locale, "menu:broadcast.begin", {
5352
+ count: sessionIds.length,
5353
+ }),
4937
5354
  });
4938
5355
  const sent = await this.replyText(ctx, [
4939
- "📣 Broadcast",
5356
+ this.t(locale, "menu:broadcast.title"),
4940
5357
  "",
4941
- `Send the next text message to broadcast it to all ${sessionIds.length} linked sessions.`,
4942
- "The message will be stored in every session inbox and the service will nudge every configured tmux target.",
4943
- "Commands like /menu or /help will cancel broadcast mode.",
5358
+ this.t(locale, "menu:broadcast.body", {
5359
+ count: sessionIds.length,
5360
+ }),
5361
+ this.t(locale, "menu:broadcast.hint"),
5362
+ this.t(locale, "menu:broadcast.cancel_hint"),
4944
5363
  ].join("\n"), { kind: "menu" }, {
4945
5364
  reply_markup: new grammy_1.InlineKeyboard().text("Cancel", "broadcast-cancel"),
4946
5365
  });
@@ -4954,10 +5373,11 @@ class TelegramTransport {
4954
5373
  });
4955
5374
  }
4956
5375
  async beginProjectBroadcast(ctx) {
5376
+ const locale = await this.resolveLocaleForContext(ctx);
4957
5377
  const principal = this.getPrincipalFromContext(ctx);
4958
5378
  if (!principal) {
4959
5379
  await ctx.answerCallbackQuery({
4960
- text: "Telegram identity is unavailable.",
5380
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4961
5381
  show_alert: true,
4962
5382
  });
4963
5383
  return;
@@ -4965,7 +5385,7 @@ class TelegramTransport {
4965
5385
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4966
5386
  if (!sessionId) {
4967
5387
  await ctx.answerCallbackQuery({
4968
- text: "Активная сессия не выбрана.",
5388
+ text: this.t(locale, "common:errors.no_active_session"),
4969
5389
  show_alert: true,
4970
5390
  });
4971
5391
  return;
@@ -4973,7 +5393,7 @@ class TelegramTransport {
4973
5393
  const projects = await this.listGatewayProjects(principal);
4974
5394
  if (projects.length === 0) {
4975
5395
  await ctx.answerCallbackQuery({
4976
- text: "Сначала создай проект или войди в существующий.",
5396
+ text: this.t(locale, "menu:broadcast.no_projects_first"),
4977
5397
  show_alert: true,
4978
5398
  });
4979
5399
  return;
@@ -4982,7 +5402,7 @@ class TelegramTransport {
4982
5402
  const totalTargets = targets.localTargetSessionIds.length + targets.remoteTargets.length;
4983
5403
  if (totalTargets === 0) {
4984
5404
  await ctx.answerCallbackQuery({
4985
- text: "Нет доступных Collab-сессий для broadcast.",
5405
+ text: this.t(locale, "menu:broadcast.no_collab_targets"),
4986
5406
  show_alert: true,
4987
5407
  });
4988
5408
  return;
@@ -4990,17 +5410,23 @@ class TelegramTransport {
4990
5410
  const principalKey = buildPrincipalKey(principal);
4991
5411
  this.pendingRenames.delete(principalKey);
4992
5412
  await ctx.answerCallbackQuery({
4993
- text: `Broadcast to ${totalTargets} collab sessions.`,
5413
+ text: this.t(locale, "menu:broadcast.collab_begin", {
5414
+ count: totalTargets,
5415
+ }),
4994
5416
  });
4995
5417
  const sent = await this.replyText(ctx, [
4996
- "📣 Collab Broadcast",
5418
+ this.t(locale, "menu:broadcast.collab_title"),
4997
5419
  "",
4998
- `Collab проектов: ${projects.length}`,
4999
- `Уникальных сессий: ${totalTargets}`,
5420
+ this.t(locale, "menu:broadcast.collab_projects", {
5421
+ count: projects.length,
5422
+ }),
5423
+ this.t(locale, "menu:broadcast.collab_sessions", {
5424
+ count: totalTargets,
5425
+ }),
5000
5426
  "",
5001
- "Отправь следующее текстовое сообщение, чтобы разослать его всем Collab-сессиям на ботах без дублирования.",
5002
- "Локальные сессии получат inbox message, удалённые — gateway delivery.",
5003
- "Команды вроде /menu или /help отменят режим broadcast.",
5427
+ this.t(locale, "menu:broadcast.collab_body"),
5428
+ this.t(locale, "menu:broadcast.collab_hint"),
5429
+ this.t(locale, "menu:broadcast.cancel_hint"),
5004
5430
  ].join("\n"), { kind: "menu", sessionId }, {
5005
5431
  reply_markup: new grammy_1.InlineKeyboard().text("Cancel", "broadcast-cancel"),
5006
5432
  });
@@ -5079,10 +5505,11 @@ class TelegramTransport {
5079
5505
  }
5080
5506
  }
5081
5507
  async cancelPendingBroadcast(ctx) {
5508
+ const locale = await this.resolveLocaleForContext(ctx);
5082
5509
  const principal = this.getPrincipalFromContext(ctx);
5083
5510
  if (!principal) {
5084
5511
  await ctx.answerCallbackQuery({
5085
- text: "Telegram identity is unavailable.",
5512
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5086
5513
  show_alert: true,
5087
5514
  });
5088
5515
  return;
@@ -5091,7 +5518,7 @@ class TelegramTransport {
5091
5518
  const pending = this.pendingBroadcasts.get(principalKey);
5092
5519
  if (!pending) {
5093
5520
  await ctx.answerCallbackQuery({
5094
- text: "Broadcast mode is not active.",
5521
+ text: this.t(locale, "menu:broadcast.mode_not_active"),
5095
5522
  show_alert: true,
5096
5523
  });
5097
5524
  return;
@@ -5100,7 +5527,9 @@ class TelegramTransport {
5100
5527
  await this.deletePendingBroadcastArtifacts(ctx, pending, {
5101
5528
  deleteMenuMessage: false,
5102
5529
  });
5103
- await ctx.answerCallbackQuery({ text: "Broadcast cancelled." });
5530
+ await ctx.answerCallbackQuery({
5531
+ text: this.t(locale, "menu:broadcast.cancelled"),
5532
+ });
5104
5533
  if (pending.scope === "project") {
5105
5534
  await this.showCollabToolsMenu(ctx);
5106
5535
  return;
@@ -5310,12 +5739,22 @@ class TelegramTransport {
5310
5739
  });
5311
5740
  await this.replyText(ctx, pending.scope === "project"
5312
5741
  ? [
5313
- "Collab broadcast completed.",
5314
- `Локальных inbox: ${storedCount}`,
5315
- `Удалённых deliveries: ${remoteCount}`,
5316
- `Всего сессий: ${storedCount + remoteCount}`,
5742
+ this.t(await this.resolveLocaleForContext(ctx), "menu:broadcast.completed_collab", {
5743
+ count: storedCount + remoteCount,
5744
+ }),
5745
+ this.t(await this.resolveLocaleForContext(ctx), "menu:broadcast.completed_collab_local", {
5746
+ count: storedCount,
5747
+ }),
5748
+ this.t(await this.resolveLocaleForContext(ctx), "menu:broadcast.completed_collab_remote", {
5749
+ count: remoteCount,
5750
+ }),
5751
+ this.t(await this.resolveLocaleForContext(ctx), "menu:broadcast.completed_collab_total", {
5752
+ count: storedCount + remoteCount,
5753
+ }),
5317
5754
  ].join("\n")
5318
- : `Broadcast completed for ${storedCount} linked sessions.`, {
5755
+ : await this.tForContext(ctx, "menu:broadcast.completed_linked", {
5756
+ count: storedCount,
5757
+ }), {
5319
5758
  kind: "menu",
5320
5759
  ...(pending.sessionId ? { sessionId: pending.sessionId } : {}),
5321
5760
  });
@@ -5332,10 +5771,11 @@ class TelegramTransport {
5332
5771
  };
5333
5772
  }
5334
5773
  async beginPartnerNoteMode(ctx, kind, target) {
5774
+ const locale = await this.resolveLocaleForContext(ctx);
5335
5775
  const principal = this.getPrincipalFromContext(ctx);
5336
5776
  if (!principal) {
5337
5777
  await ctx.answerCallbackQuery({
5338
- text: "Telegram identity is unavailable.",
5778
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5339
5779
  show_alert: true,
5340
5780
  });
5341
5781
  return;
@@ -5343,7 +5783,7 @@ class TelegramTransport {
5343
5783
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5344
5784
  if (!sessionId) {
5345
5785
  await ctx.answerCallbackQuery({
5346
- text: "No active session selected.",
5786
+ text: this.t(locale, "common:errors.no_active_session"),
5347
5787
  show_alert: true,
5348
5788
  });
5349
5789
  return;
@@ -5351,7 +5791,7 @@ class TelegramTransport {
5351
5791
  const session = await this.sessionStore.getSession(sessionId);
5352
5792
  if (!target && !session?.linkedSessionId) {
5353
5793
  await ctx.answerCallbackQuery({
5354
- text: "Link a partner session first.",
5794
+ text: this.t(locale, "menu:partner.screen.use_link_first"),
5355
5795
  show_alert: true,
5356
5796
  });
5357
5797
  return;
@@ -5362,7 +5802,7 @@ class TelegramTransport {
5362
5802
  const targetLabel = target?.targetSessionLabel ??
5363
5803
  linkedSession?.label ??
5364
5804
  session?.linkedSessionId ??
5365
- "напарник";
5805
+ this.t(locale, "menu:partner.screen.default_partner");
5366
5806
  const sourceLabel = session?.label ?? sessionId;
5367
5807
  const isProjectTarget = Boolean(target?.projectUuid);
5368
5808
  const prompt = (0, collabUi_1.buildPartnerNotePromptText)({
@@ -5374,7 +5814,7 @@ class TelegramTransport {
5374
5814
  const kindLabel = prompt.kindLabel;
5375
5815
  await ctx.answerCallbackQuery({ text: `${kindLabel}.` });
5376
5816
  const sent = await this.replyText(ctx, prompt.text, { kind: "menu", sessionId }, {
5377
- reply_markup: new grammy_1.InlineKeyboard().text("Отмена", "partner-note-cancel"),
5817
+ reply_markup: new grammy_1.InlineKeyboard().text(this.t(locale, "menu:handoff.cancel"), "partner-note-cancel"),
5378
5818
  });
5379
5819
  this.pendingPartnerNotes.set(buildPrincipalKey(principal), {
5380
5820
  sessionId,
@@ -5402,10 +5842,11 @@ class TelegramTransport {
5402
5842
  }
5403
5843
  }
5404
5844
  async cancelPendingPartnerNote(ctx) {
5845
+ const locale = await this.resolveLocaleForContext(ctx);
5405
5846
  const principal = this.getPrincipalFromContext(ctx);
5406
5847
  if (!principal) {
5407
5848
  await ctx.answerCallbackQuery({
5408
- text: "Telegram identity недоступна.",
5849
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5409
5850
  show_alert: true,
5410
5851
  });
5411
5852
  return;
@@ -5414,21 +5855,24 @@ class TelegramTransport {
5414
5855
  const pending = this.pendingPartnerNotes.get(principalKey);
5415
5856
  if (!pending) {
5416
5857
  await ctx.answerCallbackQuery({
5417
- text: "Нет активного ввода для note напарнику.",
5858
+ text: this.t(locale, "menu:partner.actions.no_pending_note_input"),
5418
5859
  show_alert: true,
5419
5860
  });
5420
5861
  return;
5421
5862
  }
5422
5863
  this.pendingPartnerNotes.delete(principalKey);
5423
5864
  await this.deletePendingPartnerNotePrompt(ctx, pending);
5424
- await ctx.answerCallbackQuery({ text: "Отправка note напарнику отменена." });
5865
+ await ctx.answerCallbackQuery({
5866
+ text: this.t(locale, "menu:partner.actions.cancel_note_input"),
5867
+ });
5425
5868
  await this.showPartnerMenu(ctx);
5426
5869
  }
5427
5870
  async beginProjectMode(ctx, mode) {
5871
+ const locale = await this.resolveLocaleForContext(ctx);
5428
5872
  const principal = this.getPrincipalFromContext(ctx);
5429
5873
  if (!principal) {
5430
5874
  await ctx.answerCallbackQuery({
5431
- text: "Telegram identity is unavailable.",
5875
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5432
5876
  show_alert: true,
5433
5877
  });
5434
5878
  return;
@@ -5436,23 +5880,23 @@ class TelegramTransport {
5436
5880
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5437
5881
  if (!sessionId) {
5438
5882
  await ctx.answerCallbackQuery({
5439
- text: "No active session selected.",
5883
+ text: this.t(locale, "common:errors.no_active_session"),
5440
5884
  show_alert: true,
5441
5885
  });
5442
5886
  return;
5443
5887
  }
5444
5888
  const sent = await this.replyText(ctx, mode === "create"
5445
5889
  ? [
5446
- "📦 Создать проект",
5890
+ this.t(locale, "menu:project.create_prompt_title"),
5447
5891
  "",
5448
- "Отправь следующим сообщением имя проекта.",
5449
- "Команды вроде /menu или /help отменят этот режим.",
5892
+ this.t(locale, "menu:project.create_prompt_body"),
5893
+ this.t(locale, "menu:project.prompt_cancel"),
5450
5894
  ].join("\n")
5451
5895
  : [
5452
- "🔑 Вступить в проект",
5896
+ this.t(locale, "menu:project.join_prompt_title"),
5453
5897
  "",
5454
- "Отправь следующим сообщением invite token проекта.",
5455
- "Команды вроде /menu или /help отменят этот режим.",
5898
+ this.t(locale, "menu:project.join_prompt_body"),
5899
+ this.t(locale, "menu:project.prompt_cancel"),
5456
5900
  ].join("\n"), { kind: "menu", sessionId });
5457
5901
  this.pendingProjects.set(buildPrincipalKey(principal), {
5458
5902
  sessionId,
@@ -5461,14 +5905,17 @@ class TelegramTransport {
5461
5905
  ...(sent ? { promptMessageId: sent.message_id } : {}),
5462
5906
  });
5463
5907
  await ctx.answerCallbackQuery({
5464
- text: mode === "create" ? "Создание проекта." : "Вход в проект.",
5908
+ text: mode === "create"
5909
+ ? this.t(locale, "menu:project.start_create")
5910
+ : this.t(locale, "menu:project.start_join"),
5465
5911
  });
5466
5912
  }
5467
5913
  async handleProjectSelect(ctx) {
5914
+ const locale = await this.resolveLocaleForContext(ctx);
5468
5915
  const payloadKey = readMenuPayloadKey(ctx);
5469
5916
  if (!payloadKey) {
5470
5917
  await ctx.answerCallbackQuery({
5471
- text: "Данные проекта не найдены.",
5918
+ text: this.t(locale, "menu:project.data_missing"),
5472
5919
  show_alert: true,
5473
5920
  });
5474
5921
  return;
@@ -5479,7 +5926,7 @@ class TelegramTransport {
5479
5926
  !payload.sessionId ||
5480
5927
  !payload.projectUuid) {
5481
5928
  await ctx.answerCallbackQuery({
5482
- text: "Данные проекта устарели или некорректны.",
5929
+ text: this.t(locale, "menu:project.data_stale"),
5483
5930
  show_alert: true,
5484
5931
  });
5485
5932
  return;
@@ -5487,7 +5934,7 @@ class TelegramTransport {
5487
5934
  const principal = this.getPrincipalFromContext(ctx);
5488
5935
  if (!principal) {
5489
5936
  await ctx.answerCallbackQuery({
5490
- text: "Telegram identity is unavailable.",
5937
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5491
5938
  show_alert: true,
5492
5939
  });
5493
5940
  return;
@@ -5495,7 +5942,7 @@ class TelegramTransport {
5495
5942
  const project = await this.getProjectPayloadByUuid(payload.sessionId, payload.projectUuid);
5496
5943
  if (!project) {
5497
5944
  await ctx.answerCallbackQuery({
5498
- text: "Проект не найден.",
5945
+ text: this.t(locale, "menu:project.not_found"),
5499
5946
  show_alert: true,
5500
5947
  });
5501
5948
  return;
@@ -5506,14 +5953,17 @@ class TelegramTransport {
5506
5953
  projectUuid: project.projectUuid,
5507
5954
  projectName: project.projectName,
5508
5955
  });
5509
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
5956
+ await ctx.answerCallbackQuery({
5957
+ text: this.t(locale, "menu:project.opening_members"),
5958
+ });
5510
5959
  await this.showProjectMembers(ctx, project);
5511
5960
  }
5512
5961
  async handleProjectDeleteSelect(ctx) {
5962
+ const locale = await this.resolveLocaleForContext(ctx);
5513
5963
  const payloadKey = readMenuPayloadKey(ctx);
5514
5964
  if (!payloadKey) {
5515
5965
  await ctx.answerCallbackQuery({
5516
- text: "Данные проекта не найдены.",
5966
+ text: this.t(locale, "menu:project.data_missing"),
5517
5967
  show_alert: true,
5518
5968
  });
5519
5969
  return;
@@ -5524,7 +5974,7 @@ class TelegramTransport {
5524
5974
  !payload.sessionId ||
5525
5975
  !payload.projectUuid) {
5526
5976
  await ctx.answerCallbackQuery({
5527
- text: "Данные проекта устарели или некорректны.",
5977
+ text: this.t(locale, "menu:project.data_stale"),
5528
5978
  show_alert: true,
5529
5979
  });
5530
5980
  await ctx.deleteMessage().catch(() => undefined);
@@ -5533,7 +5983,7 @@ class TelegramTransport {
5533
5983
  const principal = this.getPrincipalFromContext(ctx);
5534
5984
  if (!principal) {
5535
5985
  await ctx.answerCallbackQuery({
5536
- text: "Telegram identity is unavailable.",
5986
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5537
5987
  show_alert: true,
5538
5988
  });
5539
5989
  return;
@@ -5542,7 +5992,7 @@ class TelegramTransport {
5542
5992
  const project = projects.find((item) => item.project_uuid === payload.projectUuid);
5543
5993
  if (!project) {
5544
5994
  await ctx.answerCallbackQuery({
5545
- text: "Проект не найден.",
5995
+ text: this.t(locale, "menu:project.not_found"),
5546
5996
  show_alert: true,
5547
5997
  });
5548
5998
  await ctx.deleteMessage().catch(() => undefined);
@@ -5550,7 +6000,7 @@ class TelegramTransport {
5550
6000
  }
5551
6001
  if (project.role !== "owner") {
5552
6002
  await ctx.answerCallbackQuery({
5553
- text: "Удалять проект может только owner.",
6003
+ text: this.t(locale, "menu:project.delete_only_owner"),
5554
6004
  show_alert: true,
5555
6005
  });
5556
6006
  return;
@@ -5558,10 +6008,11 @@ class TelegramTransport {
5558
6008
  await this.handleProjectDeleteByUuid(ctx, payload.projectUuid);
5559
6009
  }
5560
6010
  async leaveActiveProject(ctx) {
6011
+ const locale = await this.resolveLocaleForContext(ctx);
5561
6012
  const principal = this.getPrincipalFromContext(ctx);
5562
6013
  if (!principal) {
5563
6014
  await ctx.answerCallbackQuery({
5564
- text: "Telegram identity is unavailable.",
6015
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5565
6016
  show_alert: true,
5566
6017
  });
5567
6018
  return;
@@ -5569,7 +6020,7 @@ class TelegramTransport {
5569
6020
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5570
6021
  if (!sessionId) {
5571
6022
  await ctx.answerCallbackQuery({
5572
- text: "No active session selected.",
6023
+ text: this.t(locale, "common:errors.no_active_session"),
5573
6024
  show_alert: true,
5574
6025
  });
5575
6026
  return;
@@ -5577,7 +6028,7 @@ class TelegramTransport {
5577
6028
  const session = await this.sessionStore.getSession(sessionId);
5578
6029
  if (!session?.activeProjectUuid) {
5579
6030
  await ctx.answerCallbackQuery({
5580
- text: "No active project.",
6031
+ text: this.t(locale, "menu:project.no_active_project"),
5581
6032
  show_alert: true,
5582
6033
  });
5583
6034
  return;
@@ -5593,8 +6044,10 @@ class TelegramTransport {
5593
6044
  activeProjectName: undefined,
5594
6045
  updatedAt: new Date().toISOString(),
5595
6046
  });
5596
- await ctx.answerCallbackQuery({ text: "Project left." });
5597
- await this.showProjectsMenu(ctx, "Вы вышли из текущего проекта.");
6047
+ await ctx.answerCallbackQuery({
6048
+ text: this.t(locale, "menu:project.left_current"),
6049
+ });
6050
+ await this.showProjectsMenu(ctx, this.t(locale, "menu:project.left_current_screen"));
5598
6051
  }
5599
6052
  async showProjectDetail(ctx, input) {
5600
6053
  await this.showProjectMembers(ctx, input);
@@ -5642,6 +6095,7 @@ class TelegramTransport {
5642
6095
  if (!binding) {
5643
6096
  throw new Error("Binding is missing for project members screen.");
5644
6097
  }
6098
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
5645
6099
  const sessions = await this.listGatewayProjectSessions({
5646
6100
  telegramChatId: binding.telegramChatId,
5647
6101
  telegramUserId: binding.telegramUserId,
@@ -5649,19 +6103,25 @@ class TelegramTransport {
5649
6103
  const activeSessionId = session?.sessionId ?? null;
5650
6104
  const selectableMembers = sessions.filter((item) => item.local_session_id !== activeSessionId);
5651
6105
  const lines = [
5652
- `👥 Участники ${escapeHtml(input.projectName)}`,
6106
+ this.t(locale, "menu:project.members_title", {
6107
+ projectName: escapeHtml(input.projectName),
6108
+ }),
5653
6109
  "",
5654
6110
  `UUID: ${input.projectUuid}`,
5655
6111
  `Invite: <code>${escapeHtml(input.inviteToken)}</code>`,
5656
6112
  "",
5657
- `Сессия: ${escapeHtml(session?.label ?? input.sessionId)}`,
5658
- `Других сессий: ${selectableMembers.length}`,
6113
+ this.t(locale, "menu:project.current_session", {
6114
+ sessionName: escapeHtml(session?.label ?? input.sessionId),
6115
+ }),
6116
+ this.t(locale, "menu:project.other_sessions", {
6117
+ count: selectableMembers.length,
6118
+ }),
5659
6119
  "",
5660
6120
  selectableMembers.length > 0
5661
6121
  ? options?.filePath
5662
- ? "Выбери, кому передать этот файл."
5663
- : "Выбери сессию, чтобы спросить, поделиться, ответить или передать."
5664
- : этом проекте пока нет других активных сессий.",
6122
+ ? this.t(locale, "menu:project.choose_file_target")
6123
+ : this.t(locale, "menu:project.choose_member_action")
6124
+ : this.t(locale, "menu:project.no_other_active"),
5665
6125
  ];
5666
6126
  const keyboard = new grammy_1.InlineKeyboard();
5667
6127
  for (const member of selectableMembers) {
@@ -5685,11 +6145,12 @@ class TelegramTransport {
5685
6145
  keyboard.text(buttonLabel, `project-member-open:${payloadKey}`).row();
5686
6146
  }
5687
6147
  keyboard
5688
- .text("🚪 Выйти", `project-leave:${input.projectUuid}`)
5689
- .text("⬅ К проектам", "project-back");
6148
+ .text(this.t(locale, "menu:project.leave"), `project-leave:${input.projectUuid}`)
6149
+ .text(this.t(locale, "menu:project.back_to_projects"), "project-back");
5690
6150
  return { text: lines.join("\n"), keyboard };
5691
6151
  }
5692
6152
  async showProjectMemberDetail(ctx, input) {
6153
+ const locale = await this.resolveLocaleForContext(ctx);
5693
6154
  const principal = this.getPrincipalFromContext(ctx);
5694
6155
  if (principal) {
5695
6156
  await this.ensureOpenedProjectIsActive({
@@ -5724,8 +6185,8 @@ class TelegramTransport {
5724
6185
  : {}),
5725
6186
  });
5726
6187
  const keyboard = new grammy_1.InlineKeyboard()
5727
- .text("❓ Спросить", `project-member-note:question:${payloadKey}`)
5728
- .text("📤 Поделиться", `project-member-note:share:${payloadKey}`)
6188
+ .text(this.t(locale, "menu:project.ask"), `project-member-note:question:${payloadKey}`)
6189
+ .text(this.t(locale, "menu:project.share_button"), `project-member-note:share:${payloadKey}`)
5729
6190
  .row();
5730
6191
  if (this.config.webapp.enabled &&
5731
6192
  this.config.distributed.gatewayPublicUrl &&
@@ -5734,7 +6195,7 @@ class TelegramTransport {
5734
6195
  input.targetLocalSessionId) {
5735
6196
  keyboard.text("🖥 Live", `project-member-live:${payloadKey}`).row();
5736
6197
  }
5737
- keyboard.text("⬅ К участникам", `project-members:${input.projectUuid}`);
6198
+ keyboard.text(this.t(locale, "menu:project.back_to_members"), `project-members:${input.projectUuid}`);
5738
6199
  if (ctx.callbackQuery?.message) {
5739
6200
  if (principal && ctx.chat && "message_id" in ctx.callbackQuery.message) {
5740
6201
  this.webAppLaunchRegistry.set(principal.telegramUserId, input.sessionId, this.config.webapp.initDataTtlSeconds, {
@@ -5756,6 +6217,7 @@ class TelegramTransport {
5756
6217
  }
5757
6218
  }
5758
6219
  async showProjectMemberFiles(ctx, input) {
6220
+ const locale = await this.resolveLocaleForContext(ctx);
5759
6221
  const principal = this.getPrincipalFromContext(ctx);
5760
6222
  if (principal) {
5761
6223
  await this.ensureOpenedProjectIsActive({
@@ -5767,14 +6229,18 @@ class TelegramTransport {
5767
6229
  }
5768
6230
  const files = await this.listActiveSessionFiles(input.sessionId);
5769
6231
  const lines = [
5770
- "📎 Выбор файла",
6232
+ this.t(locale, "menu:project.file_title"),
5771
6233
  "",
5772
- `Проект: ${input.projectName}`,
5773
- `Получатель: ${input.targetSessionLabel}`,
6234
+ this.t(locale, "menu:project.file_project", {
6235
+ projectName: input.projectName,
6236
+ }),
6237
+ this.t(locale, "menu:project.file_recipient", {
6238
+ label: input.targetSessionLabel,
6239
+ }),
5774
6240
  "",
5775
6241
  files.length > 0
5776
- ? "Выбери файл для отправки."
5777
- : этой сессии нет загруженных файлов.",
6242
+ ? this.t(locale, "menu:project.file_choose")
6243
+ : this.t(locale, "menu:project.file_none"),
5778
6244
  ];
5779
6245
  const keyboard = new grammy_1.InlineKeyboard();
5780
6246
  for (const filePath of files) {
@@ -5791,7 +6257,7 @@ class TelegramTransport {
5791
6257
  });
5792
6258
  keyboard.text(label, `project-member-open:${payloadKey}`).row();
5793
6259
  }
5794
- keyboard.text("⬅ К сессии", `project-member-open:${await this.createProjectMemberMenuPayload(input.sessionId, input.projectUuid, input.targetSessionId, input.targetSessionLabel, {
6260
+ keyboard.text(this.t(locale, "menu:project.back_to_session"), `project-member-open:${await this.createProjectMemberMenuPayload(input.sessionId, input.projectUuid, input.targetSessionId, input.targetSessionLabel, {
5795
6261
  ...(input.targetClientUuid
5796
6262
  ? { targetClientUuid: input.targetClientUuid }
5797
6263
  : {}),
@@ -5913,11 +6379,12 @@ class TelegramTransport {
5913
6379
  return data.slice(prefix.length) || null;
5914
6380
  }
5915
6381
  async handleProjectSetCallback(ctx) {
6382
+ const locale = await this.resolveLocaleForContext(ctx);
5916
6383
  const projectUuid = this.extractCallbackSuffix(ctx, "project-set:");
5917
6384
  const principal = this.getPrincipalFromContext(ctx);
5918
6385
  if (!projectUuid || !principal) {
5919
6386
  await ctx.answerCallbackQuery({
5920
- text: "Некорректное действие проекта.",
6387
+ text: this.t(locale, "menu:project.invalid_action"),
5921
6388
  show_alert: true,
5922
6389
  });
5923
6390
  return;
@@ -5925,7 +6392,7 @@ class TelegramTransport {
5925
6392
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5926
6393
  if (!sessionId) {
5927
6394
  await ctx.answerCallbackQuery({
5928
- text: "Активная сессия не выбрана.",
6395
+ text: this.t(locale, "common:errors.no_active_session"),
5929
6396
  show_alert: true,
5930
6397
  });
5931
6398
  return;
@@ -5933,7 +6400,7 @@ class TelegramTransport {
5933
6400
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
5934
6401
  if (!payload) {
5935
6402
  await ctx.answerCallbackQuery({
5936
- text: "Проект не найден.",
6403
+ text: this.t(locale, "menu:project.not_found"),
5937
6404
  show_alert: true,
5938
6405
  });
5939
6406
  return;
@@ -5944,15 +6411,18 @@ class TelegramTransport {
5944
6411
  projectUuid: payload.projectUuid,
5945
6412
  projectName: payload.projectName,
5946
6413
  });
5947
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
6414
+ await ctx.answerCallbackQuery({
6415
+ text: this.t(locale, "menu:project.opening_members"),
6416
+ });
5948
6417
  await this.showProjectMembers(ctx, payload);
5949
6418
  }
5950
6419
  async handleProjectDetailCallback(ctx) {
6420
+ const locale = await this.resolveLocaleForContext(ctx);
5951
6421
  const projectUuid = this.extractCallbackSuffix(ctx, "project-detail:");
5952
6422
  const principal = this.getPrincipalFromContext(ctx);
5953
6423
  if (!projectUuid || !principal) {
5954
6424
  await ctx.answerCallbackQuery({
5955
- text: "Некорректное действие проекта.",
6425
+ text: this.t(locale, "menu:project.invalid_action"),
5956
6426
  show_alert: true,
5957
6427
  });
5958
6428
  return;
@@ -5960,7 +6430,7 @@ class TelegramTransport {
5960
6430
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5961
6431
  if (!sessionId) {
5962
6432
  await ctx.answerCallbackQuery({
5963
- text: "Активная сессия не выбрана.",
6433
+ text: this.t(locale, "common:errors.no_active_session"),
5964
6434
  show_alert: true,
5965
6435
  });
5966
6436
  return;
@@ -5968,7 +6438,7 @@ class TelegramTransport {
5968
6438
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
5969
6439
  if (!payload) {
5970
6440
  await ctx.answerCallbackQuery({
5971
- text: "Проект не найден.",
6441
+ text: this.t(locale, "menu:project.not_found"),
5972
6442
  show_alert: true,
5973
6443
  });
5974
6444
  return;
@@ -5979,14 +6449,17 @@ class TelegramTransport {
5979
6449
  projectUuid: payload.projectUuid,
5980
6450
  projectName: payload.projectName,
5981
6451
  });
5982
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
6452
+ await ctx.answerCallbackQuery({
6453
+ text: this.t(locale, "menu:project.opening_members"),
6454
+ });
5983
6455
  await this.showProjectMembers(ctx, payload);
5984
6456
  }
5985
6457
  async handleProjectDeleteCallback(ctx) {
6458
+ const locale = await this.resolveLocaleForContext(ctx);
5986
6459
  const projectUuid = this.extractCallbackSuffix(ctx, "project-delete:");
5987
6460
  if (!projectUuid) {
5988
6461
  await ctx.answerCallbackQuery({
5989
- text: "Некорректное действие проекта.",
6462
+ text: this.t(locale, "menu:project.invalid_action"),
5990
6463
  show_alert: true,
5991
6464
  });
5992
6465
  return;
@@ -5994,10 +6467,11 @@ class TelegramTransport {
5994
6467
  await this.handleProjectDeleteByUuid(ctx, projectUuid);
5995
6468
  }
5996
6469
  async handleProjectMemberOpenCallback(ctx) {
6470
+ const locale = await this.resolveLocaleForContext(ctx);
5997
6471
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-open:");
5998
6472
  if (!payloadKey) {
5999
6473
  await ctx.answerCallbackQuery({
6000
- text: "Некорректные данные участника проекта.",
6474
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6001
6475
  show_alert: true,
6002
6476
  });
6003
6477
  return;
@@ -6005,7 +6479,7 @@ class TelegramTransport {
6005
6479
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
6006
6480
  if (!payload) {
6007
6481
  await ctx.answerCallbackQuery({
6008
- text: "Данные участника проекта некорректны или устарели.",
6482
+ text: this.t(locale, "menu:project.stale_member_payload"),
6009
6483
  show_alert: true,
6010
6484
  });
6011
6485
  await ctx.deleteMessage().catch(() => undefined);
@@ -6021,15 +6495,18 @@ class TelegramTransport {
6021
6495
  });
6022
6496
  return;
6023
6497
  }
6024
- await ctx.answerCallbackQuery({ text: "Открываю сессию." });
6498
+ await ctx.answerCallbackQuery({
6499
+ text: this.t(locale, "menu:project.opening_session"),
6500
+ });
6025
6501
  await this.showProjectMemberDetail(ctx, payload);
6026
6502
  }
6027
6503
  async handleProjectMemberNoteCallback(ctx) {
6504
+ const locale = await this.resolveLocaleForContext(ctx);
6028
6505
  const data = ctx.callbackQuery?.data ?? "";
6029
6506
  const match = data.match(/^project-member-note:(question|share):(.+)$/u);
6030
6507
  if (!match) {
6031
6508
  await ctx.answerCallbackQuery({
6032
- text: "Некорректное действие для участника проекта.",
6509
+ text: this.t(locale, "menu:project.invalid_member_action"),
6033
6510
  show_alert: true,
6034
6511
  });
6035
6512
  return;
@@ -6038,7 +6515,7 @@ class TelegramTransport {
6038
6515
  const payloadKey = payloadKeyRaw?.trim();
6039
6516
  if (!payloadKey) {
6040
6517
  await ctx.answerCallbackQuery({
6041
- text: "Некорректные данные участника проекта.",
6518
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6042
6519
  show_alert: true,
6043
6520
  });
6044
6521
  return;
@@ -6046,7 +6523,7 @@ class TelegramTransport {
6046
6523
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
6047
6524
  if (!payload) {
6048
6525
  await ctx.answerCallbackQuery({
6049
- text: "Данные участника проекта некорректны или устарели.",
6526
+ text: this.t(locale, "menu:project.stale_member_payload"),
6050
6527
  show_alert: true,
6051
6528
  });
6052
6529
  await ctx.deleteMessage().catch(() => undefined);
@@ -6059,10 +6536,11 @@ class TelegramTransport {
6059
6536
  });
6060
6537
  }
6061
6538
  async handleProjectMemberLiveCallback(ctx) {
6539
+ const locale = await this.resolveLocaleForContext(ctx);
6062
6540
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-live:");
6063
6541
  if (!payloadKey) {
6064
6542
  await ctx.answerCallbackQuery({
6065
- text: "Некорректные данные Live View.",
6543
+ text: this.t(locale, "menu:project.invalid_live_payload"),
6066
6544
  show_alert: true,
6067
6545
  });
6068
6546
  return;
@@ -6070,7 +6548,7 @@ class TelegramTransport {
6070
6548
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
6071
6549
  if (!payload || !payload.targetClientUuid || !payload.targetLocalSessionId) {
6072
6550
  await ctx.answerCallbackQuery({
6073
- text: "Данные Live View некорректны или устарели.",
6551
+ text: this.t(locale, "menu:project.stale_live_payload"),
6074
6552
  show_alert: true,
6075
6553
  });
6076
6554
  await ctx.deleteMessage().catch(() => undefined);
@@ -6079,7 +6557,7 @@ class TelegramTransport {
6079
6557
  const principal = this.getPrincipalFromContext(ctx);
6080
6558
  if (!principal) {
6081
6559
  await ctx.answerCallbackQuery({
6082
- text: "Не удалось определить Telegram пользователя.",
6560
+ text: this.t(locale, "menu:project.no_telegram_user"),
6083
6561
  show_alert: true,
6084
6562
  });
6085
6563
  return;
@@ -6104,17 +6582,18 @@ class TelegramTransport {
6104
6582
  });
6105
6583
  await ctx.answerCallbackQuery({
6106
6584
  text: result?.delivered
6107
- ? "Запрос на Live отправлен на подтверждение."
6108
- : "Сессия сейчас недоступна для подтверждения.",
6585
+ ? this.t(locale, "menu:project.request_live_sent")
6586
+ : this.t(locale, "menu:live.actions.approval_unavailable"),
6109
6587
  ...(result?.delivered ? {} : { show_alert: true }),
6110
6588
  });
6111
6589
  }
6112
6590
  async handleLiveApprovalCallback(ctx) {
6591
+ const locale = await this.resolveLocaleForContext(ctx);
6113
6592
  const data = ctx.callbackQuery?.data ?? "";
6114
6593
  const match = data.match(/^live-approval:(approve|deny):(.+)$/u);
6115
6594
  if (!match) {
6116
6595
  await ctx.answerCallbackQuery({
6117
- text: "Некорректное подтверждение Live View.",
6596
+ text: this.t(locale, "menu:project.invalid_approval"),
6118
6597
  show_alert: true,
6119
6598
  });
6120
6599
  return;
@@ -6123,7 +6602,7 @@ class TelegramTransport {
6123
6602
  const payloadKey = payloadKeyRaw?.trim();
6124
6603
  if (!payloadKey) {
6125
6604
  await ctx.answerCallbackQuery({
6126
- text: "Некорректные данные подтверждения.",
6605
+ text: this.t(locale, "menu:project.invalid_approval_data"),
6127
6606
  show_alert: true,
6128
6607
  });
6129
6608
  return;
@@ -6131,7 +6610,7 @@ class TelegramTransport {
6131
6610
  const payload = await this.getLiveApprovalPayloadByKey(payloadKey);
6132
6611
  if (!payload) {
6133
6612
  await ctx.answerCallbackQuery({
6134
- text: "Запрос Live View устарел.",
6613
+ text: this.t(locale, "menu:project.approval_stale"),
6135
6614
  show_alert: true,
6136
6615
  });
6137
6616
  await ctx.deleteMessage().catch(() => undefined);
@@ -6155,28 +6634,45 @@ class TelegramTransport {
6155
6634
  },
6156
6635
  });
6157
6636
  await ctx.answerCallbackQuery({
6158
- text: approved ? "Live View разрешён." : "Live View отклонён.",
6637
+ text: approved
6638
+ ? this.t(locale, "menu:live.approval.approved")
6639
+ : this.t(locale, "menu:live.approval.denied"),
6159
6640
  });
6160
6641
  if (ctx.callbackQuery?.message) {
6161
6642
  await this.editText(ctx, [
6162
- approved ? "🖥 Live View разрешён" : "🖥 Live View отклонён",
6643
+ approved
6644
+ ? this.t(locale, "menu:live.approval.approved")
6645
+ : this.t(locale, "menu:live.approval.denied"),
6163
6646
  "",
6164
- ...(payload.projectName ? [`Проект: ${payload.projectName}`] : []),
6165
- `Сессия: ${payload.sourceSessionLabel} -> ${payload.targetSessionLabel}`,
6647
+ ...(payload.projectName
6648
+ ? [
6649
+ this.t(locale, "menu:live.approval.project", {
6650
+ projectName: payload.projectName,
6651
+ }),
6652
+ ]
6653
+ : []),
6654
+ this.t(locale, "menu:live.approval.route", {
6655
+ sourceSessionName: payload.sourceSessionLabel,
6656
+ targetSessionName: payload.targetSessionLabel,
6657
+ }),
6166
6658
  "",
6167
6659
  result?.delivered
6168
6660
  ? approved
6169
- ? "Запросившая сторона получила кнопку для открытия Live."
6170
- : "Запросившая сторона получила уведомление об отклонении."
6171
- : "Запросившая сторона сейчас недоступна.",
6661
+ ? this.t(locale, "menu:live.approval.source_open")
6662
+ : this.t(locale, "menu:live.approval.result_denied", {
6663
+ sourceSessionName: payload.sourceSessionLabel,
6664
+ targetSessionName: payload.targetSessionLabel,
6665
+ })
6666
+ : this.t(locale, "menu:live.actions.approval_unavailable"),
6172
6667
  ].join("\n"), { kind: "menu", sessionId: payload.sessionId });
6173
6668
  }
6174
6669
  }
6175
6670
  async handleProjectMemberFilesCallback(ctx) {
6671
+ const locale = await this.resolveLocaleForContext(ctx);
6176
6672
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-files:");
6177
6673
  if (!payloadKey) {
6178
6674
  await ctx.answerCallbackQuery({
6179
- text: "Некорректные данные участника проекта.",
6675
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6180
6676
  show_alert: true,
6181
6677
  });
6182
6678
  return;
@@ -6184,20 +6680,23 @@ class TelegramTransport {
6184
6680
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
6185
6681
  if (!payload) {
6186
6682
  await ctx.answerCallbackQuery({
6187
- text: "Данные участника проекта некорректны или устарели.",
6683
+ text: this.t(locale, "menu:project.stale_member_payload"),
6188
6684
  show_alert: true,
6189
6685
  });
6190
6686
  await ctx.deleteMessage().catch(() => undefined);
6191
6687
  return;
6192
6688
  }
6193
- await ctx.answerCallbackQuery({ text: "Открываю файлы." });
6689
+ await ctx.answerCallbackQuery({
6690
+ text: this.t(locale, "menu:project.opening_files"),
6691
+ });
6194
6692
  await this.showProjectMemberFiles(ctx, payload);
6195
6693
  }
6196
6694
  async handlePartnerFileOpenCallback(ctx) {
6695
+ const locale = await this.resolveLocaleForContext(ctx);
6197
6696
  const payloadKey = this.extractCallbackSuffix(ctx, "partner-file-open:");
6198
6697
  if (!payloadKey) {
6199
6698
  await ctx.answerCallbackQuery({
6200
- text: "Некорректные данные файла.",
6699
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6201
6700
  show_alert: true,
6202
6701
  });
6203
6702
  return;
@@ -6205,7 +6704,7 @@ class TelegramTransport {
6205
6704
  const payload = await this.getPartnerFileTargetPayloadByKey(payloadKey);
6206
6705
  if (!payload) {
6207
6706
  await ctx.answerCallbackQuery({
6208
- text: "Данные файла устарели.",
6707
+ text: this.t(locale, "menu:project.data_stale"),
6209
6708
  show_alert: true,
6210
6709
  });
6211
6710
  return;
@@ -6218,11 +6717,12 @@ class TelegramTransport {
6218
6717
  });
6219
6718
  }
6220
6719
  async handleProjectMembersCallback(ctx) {
6720
+ const locale = await this.resolveLocaleForContext(ctx);
6221
6721
  const projectUuid = this.extractCallbackSuffix(ctx, "project-members:");
6222
6722
  const principal = this.getPrincipalFromContext(ctx);
6223
6723
  if (!projectUuid || !principal) {
6224
6724
  await ctx.answerCallbackQuery({
6225
- text: "Некорректное действие проекта.",
6725
+ text: this.t(locale, "menu:project.invalid_action"),
6226
6726
  show_alert: true,
6227
6727
  });
6228
6728
  return;
@@ -6230,7 +6730,7 @@ class TelegramTransport {
6230
6730
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6231
6731
  if (!sessionId) {
6232
6732
  await ctx.answerCallbackQuery({
6233
- text: "Активная сессия не выбрана.",
6733
+ text: this.t(locale, "common:errors.no_active_session"),
6234
6734
  show_alert: true,
6235
6735
  });
6236
6736
  return;
@@ -6238,20 +6738,23 @@ class TelegramTransport {
6238
6738
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
6239
6739
  if (!payload) {
6240
6740
  await ctx.answerCallbackQuery({
6241
- text: "Проект не найден.",
6741
+ text: this.t(locale, "menu:project.not_found"),
6242
6742
  show_alert: true,
6243
6743
  });
6244
6744
  return;
6245
6745
  }
6246
- await ctx.answerCallbackQuery({ text: "Загружаю участников." });
6746
+ await ctx.answerCallbackQuery({
6747
+ text: this.t(locale, "menu:project.loading_members"),
6748
+ });
6247
6749
  await this.showProjectMembers(ctx, payload);
6248
6750
  }
6249
6751
  async handleProjectLeaveCallback(ctx) {
6752
+ const locale = await this.resolveLocaleForContext(ctx);
6250
6753
  const projectUuid = this.extractCallbackSuffix(ctx, "project-leave:");
6251
6754
  const principal = this.getPrincipalFromContext(ctx);
6252
6755
  if (!projectUuid || !principal) {
6253
6756
  await ctx.answerCallbackQuery({
6254
- text: "Некорректное действие проекта.",
6757
+ text: this.t(locale, "menu:project.invalid_action"),
6255
6758
  show_alert: true,
6256
6759
  });
6257
6760
  return;
@@ -6259,7 +6762,7 @@ class TelegramTransport {
6259
6762
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6260
6763
  if (!sessionId) {
6261
6764
  await ctx.answerCallbackQuery({
6262
- text: "Активная сессия не выбрана.",
6765
+ text: this.t(locale, "common:errors.no_active_session"),
6263
6766
  show_alert: true,
6264
6767
  });
6265
6768
  return;
@@ -6278,14 +6781,17 @@ class TelegramTransport {
6278
6781
  updatedAt: new Date().toISOString(),
6279
6782
  });
6280
6783
  }
6281
- await ctx.answerCallbackQuery({ text: "Выход из проекта выполнен." });
6282
- await this.showProjectsMenu(ctx, "Вы вышли из выбранного проекта.");
6784
+ await ctx.answerCallbackQuery({
6785
+ text: this.t(locale, "menu:project.left_callback"),
6786
+ });
6787
+ await this.showProjectsMenu(ctx, this.t(locale, "menu:project.left_screen"));
6283
6788
  }
6284
6789
  async handleProjectDeleteByUuid(ctx, projectUuid) {
6790
+ const locale = await this.resolveLocaleForContext(ctx);
6285
6791
  const principal = this.getPrincipalFromContext(ctx);
6286
6792
  if (!principal) {
6287
6793
  await ctx.answerCallbackQuery({
6288
- text: "Telegram identity is unavailable.",
6794
+ text: this.t(locale, "common:errors.no_telegram_identity"),
6289
6795
  show_alert: true,
6290
6796
  });
6291
6797
  return;
@@ -6293,7 +6799,7 @@ class TelegramTransport {
6293
6799
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6294
6800
  if (!sessionId) {
6295
6801
  await ctx.answerCallbackQuery({
6296
- text: "Активная сессия не выбрана.",
6802
+ text: this.t(locale, "common:errors.no_active_session"),
6297
6803
  show_alert: true,
6298
6804
  });
6299
6805
  return;
@@ -6302,7 +6808,7 @@ class TelegramTransport {
6302
6808
  const project = projects.find((item) => item.project_uuid === projectUuid);
6303
6809
  if (!project) {
6304
6810
  await ctx.answerCallbackQuery({
6305
- text: "Проект не найден.",
6811
+ text: this.t(locale, "menu:project.not_found"),
6306
6812
  show_alert: true,
6307
6813
  });
6308
6814
  await ctx.deleteMessage().catch(() => undefined);
@@ -6310,7 +6816,7 @@ class TelegramTransport {
6310
6816
  }
6311
6817
  if (project.role !== "owner") {
6312
6818
  await ctx.answerCallbackQuery({
6313
- text: "Удалять проект может только owner.",
6819
+ text: this.t(locale, "menu:project.delete_only_owner"),
6314
6820
  show_alert: true,
6315
6821
  });
6316
6822
  return;
@@ -6329,8 +6835,12 @@ class TelegramTransport {
6329
6835
  updatedAt: new Date().toISOString(),
6330
6836
  });
6331
6837
  }
6332
- await ctx.answerCallbackQuery({ text: "Проект удалён." });
6333
- await this.showCollabDeleteMenu(ctx, `Проект «${project.name}» удалён. Участники отвязаны, project state очищен.`);
6838
+ await ctx.answerCallbackQuery({
6839
+ text: this.t(locale, "menu:project.deleted_callback"),
6840
+ });
6841
+ await this.showCollabDeleteMenu(ctx, this.t(locale, "menu:project.deleted_screen", {
6842
+ projectName: project.name,
6843
+ }));
6334
6844
  }
6335
6845
  async deletePendingFileHandoffPrompt(ctx, pending) {
6336
6846
  if (!pending.promptMessageId) {
@@ -6349,10 +6859,11 @@ class TelegramTransport {
6349
6859
  }
6350
6860
  }
6351
6861
  async cancelPendingFileHandoff(ctx) {
6862
+ const locale = await this.resolveLocaleForContext(ctx);
6352
6863
  const principal = this.getPrincipalFromContext(ctx);
6353
6864
  if (!principal) {
6354
6865
  await ctx.answerCallbackQuery({
6355
- text: "Telegram identity is unavailable.",
6866
+ text: this.t(locale, "common:errors.no_telegram_identity"),
6356
6867
  show_alert: true,
6357
6868
  });
6358
6869
  return;
@@ -6361,14 +6872,16 @@ class TelegramTransport {
6361
6872
  const pending = this.pendingFileHandoffs.get(principalKey);
6362
6873
  if (!pending) {
6363
6874
  await ctx.answerCallbackQuery({
6364
- text: "No pending file handoff prompt.",
6875
+ text: this.t(locale, "menu:handoff.no_pending"),
6365
6876
  show_alert: true,
6366
6877
  });
6367
6878
  return;
6368
6879
  }
6369
6880
  this.pendingFileHandoffs.delete(principalKey);
6370
6881
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6371
- await ctx.answerCallbackQuery({ text: "File handoff cancelled." });
6882
+ await ctx.answerCallbackQuery({
6883
+ text: this.t(locale, "menu:handoff.cancelled"),
6884
+ });
6372
6885
  if (pending.projectUuid && pending.targetSessionId && pending.targetSessionLabel) {
6373
6886
  const project = await this.getProjectPayloadByUuid(pending.sessionId, pending.projectUuid);
6374
6887
  if (project) {
@@ -6512,6 +7025,7 @@ class TelegramTransport {
6512
7025
  });
6513
7026
  }
6514
7027
  async handlePendingPartnerNote(ctx, text) {
7028
+ const locale = await this.resolveLocaleForContext(ctx);
6515
7029
  const principal = this.getPrincipalFromContext(ctx);
6516
7030
  if (!principal) {
6517
7031
  return false;
@@ -6533,9 +7047,11 @@ class TelegramTransport {
6533
7047
  if (!resolvedTargetLabel && sourceSession?.linkedSessionId) {
6534
7048
  const linkedSession = await this.sessionStore.getSession(sourceSession.linkedSessionId);
6535
7049
  resolvedTargetLabel =
6536
- linkedSession?.label ?? sourceSession.linkedSessionId ?? "напарник";
7050
+ linkedSession?.label ??
7051
+ sourceSession.linkedSessionId ??
7052
+ this.t(locale, "menu:partner.screen.default_partner");
6537
7053
  }
6538
- const targetLabel = resolvedTargetLabel ?? "напарник";
7054
+ const targetLabel = resolvedTargetLabel ?? this.t(locale, "menu:partner.screen.default_partner");
6539
7055
  this.pendingPartnerNotes.delete(principalKey);
6540
7056
  await this.deletePendingPartnerNotePrompt(ctx, pending);
6541
7057
  if ((0, collabSemantics_1.isExecutorTargetKind)(pending.kind)) {
@@ -6571,13 +7087,30 @@ class TelegramTransport {
6571
7087
  requires_reply: true,
6572
7088
  });
6573
7089
  const sent = await this.replyText(ctx, [
6574
- "Задача отправлена выбранной сессии.",
7090
+ this.t(locale, "menu:partner.actions.task_sent"),
6575
7091
  ...(output.project_name ? [`Проект: ${output.project_name}`] : []),
6576
- ...(output.target_actor_label ? [`Исполнитель: ${output.target_actor_label}`] : []),
6577
- `Маршрут результата: ${targetLabel} -> ${sourceLabel}`,
6578
- `Тип: ${pending.kind}`,
6579
- `Кратко: ${parsed.summary}`,
6580
- `Статус: ${output.delivery_status === "delivered" ? "доставлено" : "в очереди"}`,
7092
+ ...(output.target_actor_label
7093
+ ? [
7094
+ this.t(locale, "menu:partner.screen.executor", {
7095
+ label: output.target_actor_label,
7096
+ }),
7097
+ ]
7098
+ : []),
7099
+ this.t(locale, "menu:partner.screen.route_result", {
7100
+ source: targetLabel,
7101
+ target: sourceLabel,
7102
+ }),
7103
+ this.t(locale, "menu:partner.screen.type", {
7104
+ kind: pending.kind,
7105
+ }),
7106
+ this.t(locale, "menu:partner.screen.summary", {
7107
+ summary: parsed.summary,
7108
+ }),
7109
+ this.t(locale, "menu:partner.screen.status", {
7110
+ status: output.delivery_status === "delivered"
7111
+ ? this.t(locale, "menu:partner.screen.delivered")
7112
+ : this.t(locale, "menu:partner.screen.queued"),
7113
+ }),
6581
7114
  `Share: ${output.share_id}`,
6582
7115
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6583
7116
  if (output.delivery_status === "queued" &&
@@ -6619,11 +7152,18 @@ class TelegramTransport {
6619
7152
  ...(pending.projectUuid ? { projectUuid: pending.projectUuid } : {}),
6620
7153
  });
6621
7154
  await this.replyText(ctx, [
6622
- "Задача поставлена в inbox текущей сессии.",
6623
- `Маршрут отправки: ${sourceLabel} -> ${targetLabel}`,
6624
- `Тип: ${pending.kind}`,
6625
- `Кратко: ${parsed.summary}`,
6626
- "Текущая сессия подготовит результат и отправит его сама.",
7155
+ this.t(locale, "menu:partner.actions.inbox_queued"),
7156
+ this.t(locale, "menu:partner.screen.route_send", {
7157
+ source: sourceLabel,
7158
+ target: targetLabel,
7159
+ }),
7160
+ this.t(locale, "menu:partner.screen.type", {
7161
+ kind: pending.kind,
7162
+ }),
7163
+ this.t(locale, "menu:partner.screen.summary", {
7164
+ summary: parsed.summary,
7165
+ }),
7166
+ this.t(locale, "menu:partner.screen.current_session_handles"),
6627
7167
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6628
7168
  return true;
6629
7169
  }
@@ -6667,6 +7207,7 @@ class TelegramTransport {
6667
7207
  await this.nudgeSessionInbox(input.sessionId);
6668
7208
  }
6669
7209
  async handlePendingFileHandoff(ctx, text) {
7210
+ const locale = await this.resolveLocaleForContext(ctx);
6670
7211
  const principal = this.getPrincipalFromContext(ctx);
6671
7212
  if (!principal) {
6672
7213
  return false;
@@ -6695,7 +7236,7 @@ class TelegramTransport {
6695
7236
  });
6696
7237
  this.pendingFileHandoffs.delete(principalKey);
6697
7238
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6698
- await this.replyText(ctx, "Файл передан агенту.", { kind: "menu", sessionId: pending.sessionId });
7239
+ await this.replyText(ctx, this.t(locale, "menu:handoff.delivered_agent"), { kind: "menu", sessionId: pending.sessionId });
6699
7240
  return true;
6700
7241
  }
6701
7242
  if (pending.projectUuid) {
@@ -6717,12 +7258,36 @@ class TelegramTransport {
6717
7258
  this.pendingFileHandoffs.delete(principalKey);
6718
7259
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6719
7260
  const sent = await this.replyText(ctx, [
6720
- "Файл поставлен в очередь доставки напарнику.",
6721
- ...(output.project_name ? [`Проект: ${output.project_name}`] : []),
6722
- ...(output.target_actor_label ? [`Получатель: ${output.target_actor_label}`] : []),
6723
- ...(output.target_session_label ? [`Сессия: ${output.target_session_label}`] : []),
6724
- `Статус: ${output.delivery_status === "delivered" ? "доставлено" : "в очереди"}`,
6725
- `Share: ${output.share_id}`,
7261
+ this.t(locale, "menu:handoff.queued_partner"),
7262
+ ...(output.project_name
7263
+ ? [
7264
+ this.t(locale, "menu:handoff.project", {
7265
+ projectName: output.project_name,
7266
+ }),
7267
+ ]
7268
+ : []),
7269
+ ...(output.target_actor_label
7270
+ ? [
7271
+ this.t(locale, "menu:handoff.recipient", {
7272
+ label: output.target_actor_label,
7273
+ }),
7274
+ ]
7275
+ : []),
7276
+ ...(output.target_session_label
7277
+ ? [
7278
+ this.t(locale, "menu:handoff.session", {
7279
+ label: output.target_session_label,
7280
+ }),
7281
+ ]
7282
+ : []),
7283
+ this.t(locale, "menu:handoff.status", {
7284
+ status: output.delivery_status === "delivered"
7285
+ ? this.t(locale, "menu:handoff.delivered")
7286
+ : this.t(locale, "menu:handoff.queued"),
7287
+ }),
7288
+ this.t(locale, "menu:handoff.share", {
7289
+ shareId: output.share_id,
7290
+ }),
6726
7291
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6727
7292
  if (output.delivery_status === "queued" &&
6728
7293
  sent &&
@@ -6758,6 +7323,7 @@ class TelegramTransport {
6758
7323
  return true;
6759
7324
  }
6760
7325
  async handlePendingProject(ctx, text) {
7326
+ const locale = await this.resolveLocaleForContext(ctx);
6761
7327
  const principal = this.getPrincipalFromContext(ctx);
6762
7328
  if (!principal) {
6763
7329
  return false;
@@ -6791,7 +7357,10 @@ class TelegramTransport {
6791
7357
  projectUuid,
6792
7358
  projectName,
6793
7359
  });
6794
- await this.replyText(ctx, `Проект создан: ${projectName}\nInvite: ${created.invite_token}`, { kind: "menu", sessionId: pending.sessionId });
7360
+ await this.replyText(ctx, this.t(locale, "menu:project.created", {
7361
+ projectName,
7362
+ inviteToken: created.invite_token,
7363
+ }), { kind: "menu", sessionId: pending.sessionId });
6795
7364
  }
6796
7365
  else {
6797
7366
  const joined = await this.callGatewayJson("/projects/join", {
@@ -6806,10 +7375,14 @@ class TelegramTransport {
6806
7375
  projectUuid,
6807
7376
  projectName,
6808
7377
  });
6809
- await this.replyText(ctx, `Вход в проект выполнен: ${projectName}`, { kind: "menu", sessionId: pending.sessionId });
7378
+ await this.replyText(ctx, this.t(locale, "menu:project.joined", {
7379
+ projectName,
7380
+ }), { kind: "menu", sessionId: pending.sessionId });
6810
7381
  }
6811
7382
  this.pendingProjects.delete(principalKey);
6812
- await this.showProjectsMenu(ctx, `Открыт проект: ${projectName}`);
7383
+ await this.showProjectsMenu(ctx, this.t(locale, "menu:project.opened", {
7384
+ projectName,
7385
+ }));
6813
7386
  return true;
6814
7387
  }
6815
7388
  }