@deadragdoll/tellymcp 0.0.8 → 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 (33) hide show
  1. package/.env.example.client +2 -0
  2. package/.env.example.gateway +2 -0
  3. package/README-ru.md +21 -1
  4. package/README.md +5 -1
  5. package/TOOLS.md +3 -1
  6. package/VERSION.md +58 -0
  7. package/config/templates/env.both.template +1 -0
  8. package/config/templates/env.client.template +1 -0
  9. package/config/templates/env.gateway.template +1 -0
  10. package/dist/services/features/telegram-mcp/approval.service.js +1 -1
  11. package/dist/services/features/telegram-mcp/browser.service.js +1 -1
  12. package/dist/services/features/telegram-mcp/collaboration.service.js +1 -1
  13. package/dist/services/features/telegram-mcp/gateway-rmq.service.js +3 -2
  14. package/dist/services/features/telegram-mcp/gateway-socket.service.js +8 -2
  15. package/dist/services/features/telegram-mcp/inbox.service.js +1 -1
  16. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -1
  17. package/dist/services/features/telegram-mcp/notify.service.js +1 -1
  18. package/dist/services/features/telegram-mcp/pair.service.js +1 -1
  19. package/dist/services/features/telegram-mcp/runtime.service.js +27 -7
  20. package/dist/services/features/telegram-mcp/session-context.service.js +1 -1
  21. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +2 -1
  22. package/dist/services/features/telegram-mcp/src/app/config/env.js +4 -0
  23. package/dist/services/features/telegram-mcp/src/app/http.js +8 -1
  24. package/dist/services/features/telegram-mcp/src/app/providers/mcp/server.js +2 -1
  25. package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +201 -60
  26. package/dist/services/features/telegram-mcp/src/shared/i18n/index.js +46 -0
  27. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +555 -0
  28. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +555 -0
  29. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +9 -0
  30. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +1227 -567
  31. package/dist/services/features/telegram-mcp/standalone-http.service.js +1 -1
  32. package/dist/services/features/telegram-mcp/tools-sync.service.js +1 -1
  33. 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,14 +1313,29 @@ 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
- reply_markup: new grammy_1.InlineKeyboard().webApp("Open Live View", liveViewUrl),
1332
+ reply_markup: this.buildLiveViewLaunchKeyboard((mode) => this.buildLiveViewUrlForSessionTarget({
1333
+ targetSessionId: input.target_session_id,
1334
+ targetClientUuid: input.target_client_uuid,
1335
+ targetLocalSessionId: input.target_local_session_id,
1336
+ sourceClientUuid: input.source_client_uuid,
1337
+ launchMode: mode,
1338
+ }), locale),
1242
1339
  }, {
1243
1340
  kind: "notification",
1244
1341
  sessionId: sourceSession.sessionId,
@@ -1469,22 +1566,26 @@ class TelegramTransport {
1469
1566
  fingerprint: async (ctx) => this.buildMainMenuFingerprint(ctx),
1470
1567
  ...this.createMenuOptions((ctx) => this.showMainMenu(ctx)),
1471
1568
  })
1472
- .text("🖥 Live", async (ctx) => {
1569
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.live"), async (ctx) => {
1473
1570
  await this.showLiveViewLauncher(ctx);
1474
1571
  })
1475
- .text("📄 Content", async (ctx) => {
1476
- 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
+ });
1477
1576
  await this.showBufferMenu(ctx);
1478
1577
  })
1479
- .text("🌐 Browser", async (ctx) => {
1480
- 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
+ });
1481
1582
  await this.showBrowserMenu(ctx);
1482
1583
  })
1483
1584
  .row()
1484
- .text("🏠 Local", async (ctx) => {
1585
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.local"), async (ctx) => {
1485
1586
  await this.showLocalEntryPoint(ctx);
1486
1587
  })
1487
- .text("👥 Collab", async (ctx) => {
1588
+ .text(async (ctx) => this.tForContext(ctx, "menu:main.buttons.collab"), async (ctx) => {
1488
1589
  await this.showProjectsEntryPoint(ctx);
1489
1590
  })
1490
1591
  .row()
@@ -1493,32 +1594,44 @@ class TelegramTransport {
1493
1594
  chatId: ctx.chat?.id,
1494
1595
  userId: ctx.from?.id,
1495
1596
  });
1496
- await ctx.answerCallbackQuery({ text: "Opening inbox." });
1597
+ await ctx.answerCallbackQuery({
1598
+ text: await this.tForContext(ctx, "menu:main.actions.open_inbox"),
1599
+ });
1497
1600
  await this.showInboxMenu(ctx);
1498
1601
  })
1499
- .text("📦 Storage", async (ctx) => {
1500
- 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
+ });
1501
1606
  await this.showStorageMenu(ctx);
1502
1607
  })
1503
- .text("⚙ Settings", async (ctx) => {
1504
- 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
+ });
1505
1612
  await this.showSettingsMenu(ctx);
1506
1613
  })
1507
1614
  .row()
1508
- .text("⬅ Back", async (ctx) => {
1509
- 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
+ });
1510
1619
  await this.showSessionsMenu(ctx);
1511
1620
  });
1512
1621
  }
1513
1622
  createBrowserMenu() {
1514
1623
  return new menu_1.Menu("telegram-browser-menu", this.createMenuOptions((ctx) => this.showBrowserMenu(ctx)))
1515
1624
  .text(async (ctx) => this.buildScreenshotsButtonLabel(ctx), async (ctx) => {
1516
- await ctx.answerCallbackQuery({ text: "Opening screenshots." });
1625
+ await ctx.answerCallbackQuery({
1626
+ text: await this.tForContext(ctx, "menu:browser.actions.open_screenshots"),
1627
+ });
1517
1628
  await this.showScreenshotsMenu(ctx);
1518
1629
  })
1519
1630
  .row()
1520
- .text("⬅ Back", async (ctx) => {
1521
- 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
+ });
1522
1635
  await this.showMainMenu(ctx);
1523
1636
  });
1524
1637
  }
@@ -1531,9 +1644,9 @@ class TelegramTransport {
1531
1644
  const range = new menu_1.MenuRange();
1532
1645
  const { session, projects } = await this.loadProjectsContext(ctx);
1533
1646
  if (!session || !projects) {
1534
- range.text("Gateway недоступен", async (innerCtx) => {
1647
+ range.text(await this.tForContext(ctx, "common:menu.gateway_unavailable"), async (innerCtx) => {
1535
1648
  await innerCtx.answerCallbackQuery({
1536
- text: "Проекты доступны только через gateway.",
1649
+ text: await this.tForContext(innerCtx, "menu:collab.actions.gateway_only"),
1537
1650
  show_alert: true,
1538
1651
  });
1539
1652
  });
@@ -1541,9 +1654,9 @@ class TelegramTransport {
1541
1654
  }
1542
1655
  if (projects.length === 0) {
1543
1656
  range
1544
- .text("🫥 Нет проектов", async (innerCtx) => {
1657
+ .text(await this.tForContext(ctx, "menu:collab.labels.no_projects"), async (innerCtx) => {
1545
1658
  await innerCtx.answerCallbackQuery({
1546
- text: "Проектов пока нет. Создай или войди в существующий.",
1659
+ text: await this.tForContext(innerCtx, "menu:collab.actions.no_projects"),
1547
1660
  });
1548
1661
  })
1549
1662
  .row();
@@ -1562,38 +1675,46 @@ class TelegramTransport {
1562
1675
  }
1563
1676
  return range;
1564
1677
  })
1565
- .text("➕ Создать", async (ctx) => {
1678
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.create"), async (ctx) => {
1566
1679
  await this.beginProjectMode(ctx, "create");
1567
1680
  })
1568
- .text("🛠 Tools", async (ctx) => {
1569
- 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
+ });
1570
1685
  await this.showCollabToolsMenu(ctx);
1571
1686
  })
1572
1687
  .row()
1573
- .text("🔑 Войти", async (ctx) => {
1688
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.join"), async (ctx) => {
1574
1689
  await this.beginProjectMode(ctx, "join");
1575
1690
  })
1576
- .text("⬅ Назад", async (ctx) => {
1577
- 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
+ });
1578
1695
  await this.showMainMenu(ctx);
1579
1696
  });
1580
1697
  }
1581
1698
  createCollabToolsMenu() {
1582
1699
  return new menu_1.Menu("telegram-collab-tools-menu", this.createMenuOptions((ctx) => this.showCollabToolsMenu(ctx)))
1583
- .text("📣 Broadcast", async (ctx) => {
1700
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.broadcast"), async (ctx) => {
1584
1701
  await this.beginProjectBroadcast(ctx);
1585
1702
  })
1586
- .text("🕘 History", async (ctx) => {
1703
+ .text(async (ctx) => this.tForContext(ctx, "menu:collab.buttons.history"), async (ctx) => {
1587
1704
  await this.handleCollabHistoryExport(ctx);
1588
1705
  })
1589
1706
  .row()
1590
- .text("🗑 Delete", async (ctx) => {
1591
- 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
+ });
1592
1711
  await this.showCollabDeleteMenu(ctx);
1593
1712
  })
1594
1713
  .row()
1595
- .text("⬅ Back", async (ctx) => {
1596
- 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
+ });
1597
1718
  await this.showProjectsMenu(ctx);
1598
1719
  });
1599
1720
  }
@@ -1606,18 +1727,18 @@ class TelegramTransport {
1606
1727
  const range = new menu_1.MenuRange();
1607
1728
  const { session, projects } = await this.loadProjectsContext(ctx);
1608
1729
  if (!session || !projects) {
1609
- range.text("Gateway недоступен", async (innerCtx) => {
1730
+ range.text(await this.tForContext(ctx, "common:menu.gateway_unavailable"), async (innerCtx) => {
1610
1731
  await innerCtx.answerCallbackQuery({
1611
- text: "Удаление проектов доступно только через gateway.",
1732
+ text: await this.tForContext(innerCtx, "menu:collab.actions.gateway_only"),
1612
1733
  show_alert: true,
1613
1734
  });
1614
1735
  });
1615
1736
  return range;
1616
1737
  }
1617
1738
  if (projects.length === 0) {
1618
- range.text("🫥 Нет проектов", async (innerCtx) => {
1739
+ range.text(await this.tForContext(ctx, "menu:collab.labels.no_projects"), async (innerCtx) => {
1619
1740
  await innerCtx.answerCallbackQuery({
1620
- text: "Удалять пока нечего.",
1741
+ text: await this.tForContext(innerCtx, "menu:collab.actions.no_projects"),
1621
1742
  });
1622
1743
  });
1623
1744
  return range;
@@ -1635,22 +1756,26 @@ class TelegramTransport {
1635
1756
  }
1636
1757
  return range;
1637
1758
  })
1638
- .text("⬅ Back", async (ctx) => {
1639
- 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
+ });
1640
1763
  await this.showCollabToolsMenu(ctx);
1641
1764
  });
1642
1765
  }
1643
1766
  createLocalMenu() {
1644
1767
  return new menu_1.Menu("telegram-local-menu", this.createMenuOptions((ctx) => this.showLocalMenu(ctx)))
1645
- .text("🤝 Напарник", async (ctx) => {
1768
+ .text(async (ctx) => this.tForContext(ctx, "menu:local.buttons.partner"), async (ctx) => {
1646
1769
  await this.showPartnerEntryPoint(ctx);
1647
1770
  })
1648
1771
  .text(async (ctx) => this.buildLinkButtonLabel(ctx), async (ctx) => {
1649
1772
  await this.handleLinkButton(ctx);
1650
1773
  })
1651
1774
  .row()
1652
- .text("⬅ Назад", async (ctx) => {
1653
- 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
+ });
1654
1779
  await this.showMainMenu(ctx);
1655
1780
  });
1656
1781
  }
@@ -1663,9 +1788,9 @@ class TelegramTransport {
1663
1788
  const range = new menu_1.MenuRange();
1664
1789
  const principal = this.getPrincipalFromContext(ctx);
1665
1790
  if (!principal) {
1666
- range.text("No Telegram identity", async (innerCtx) => {
1791
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1667
1792
  await innerCtx.answerCallbackQuery({
1668
- text: "Telegram user or chat is missing.",
1793
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1669
1794
  show_alert: true,
1670
1795
  });
1671
1796
  });
@@ -1673,9 +1798,9 @@ class TelegramTransport {
1673
1798
  }
1674
1799
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1675
1800
  if (!activeSessionId) {
1676
- range.text("No active session", async (innerCtx) => {
1801
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1677
1802
  await innerCtx.answerCallbackQuery({
1678
- text: "No active session is linked yet.",
1803
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1679
1804
  show_alert: true,
1680
1805
  });
1681
1806
  });
@@ -1685,9 +1810,9 @@ class TelegramTransport {
1685
1810
  .filter((sessionId) => sessionId !== activeSessionId)
1686
1811
  .sort();
1687
1812
  if (sessionIds.length === 0) {
1688
- range.text("🫥 No partner sessions", async (innerCtx) => {
1813
+ range.text(await this.tForContext(ctx, "menu:link.labels.no_partner_sessions"), async (innerCtx) => {
1689
1814
  await innerCtx.answerCallbackQuery({
1690
- text: "No other linked sessions are available.",
1815
+ text: await this.tForContext(innerCtx, "menu:link.actions.no_partner_sessions"),
1691
1816
  show_alert: true,
1692
1817
  });
1693
1818
  });
@@ -1706,72 +1831,82 @@ class TelegramTransport {
1706
1831
  }
1707
1832
  return range;
1708
1833
  })
1709
- .text("⬅ Back", async (ctx) => {
1710
- 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
+ });
1711
1838
  await this.showMainMenu(ctx);
1712
1839
  });
1713
1840
  }
1714
1841
  createPartnerMenu() {
1715
1842
  return new menu_1.Menu("telegram-partner-menu", this.createMenuOptions((ctx) => this.showPartnerMenu(ctx)))
1716
- .text("❓ Ask", async (ctx) => {
1843
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.ask"), async (ctx) => {
1717
1844
  await this.beginPartnerNoteMode(ctx, "question");
1718
1845
  })
1719
- .text("📤 Share", async (ctx) => {
1846
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.share"), async (ctx) => {
1720
1847
  await this.beginPartnerNoteMode(ctx, "share");
1721
1848
  })
1722
1849
  .row()
1723
- .text("🔓 Unlink", async (ctx) => {
1850
+ .text(async (ctx) => this.tForContext(ctx, "menu:partner.buttons.unlink"), async (ctx) => {
1724
1851
  await this.handleLinkButton(ctx);
1725
1852
  })
1726
- .text("⬅ Back", async (ctx) => {
1727
- 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
+ });
1728
1857
  await this.showMainMenu(ctx);
1729
1858
  });
1730
1859
  }
1731
1860
  createBufferMenu() {
1732
1861
  return new menu_1.Menu("telegram-buffer-menu", this.createMenuOptions((ctx) => this.showBufferMenu(ctx)))
1733
- .text("👁 Visible", async (ctx) => {
1862
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.visible"), async (ctx) => {
1734
1863
  await this.sendActiveSessionBuffer(ctx, { mode: "visible" });
1735
1864
  })
1736
- .text("🧾 Full", async (ctx) => {
1865
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.full"), async (ctx) => {
1737
1866
  await this.sendActiveSessionBuffer(ctx, { mode: "full" });
1738
1867
  })
1739
1868
  .row()
1740
- .text("📄 Last 300", async (ctx) => {
1869
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.last_300"), async (ctx) => {
1741
1870
  await this.sendActiveSessionBuffer(ctx, {
1742
1871
  mode: "lines",
1743
1872
  lines: 300,
1744
1873
  });
1745
1874
  })
1746
- .text("📄 Last 1000", async (ctx) => {
1875
+ .text(async (ctx) => this.tForContext(ctx, "menu:buffer.buttons.last_1000"), async (ctx) => {
1747
1876
  await this.sendActiveSessionBuffer(ctx, {
1748
1877
  mode: "lines",
1749
1878
  lines: 1000,
1750
1879
  });
1751
1880
  })
1752
1881
  .row()
1753
- .text("⬅ Back", async (ctx) => {
1754
- 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
+ });
1755
1886
  await this.showMainMenu(ctx);
1756
1887
  });
1757
1888
  }
1758
1889
  createSettingsMenu() {
1759
1890
  return new menu_1.Menu("telegram-settings-menu", this.createMenuOptions((ctx) => this.showSettingsMenu(ctx)))
1760
- .text("ℹ Info", async (ctx) => {
1891
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.info"), async (ctx) => {
1761
1892
  await this.showActiveSessionInfo(ctx);
1762
1893
  })
1763
1894
  .row()
1764
- .text("✏ Rename", async (ctx) => {
1895
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.rename"), async (ctx) => {
1765
1896
  await this.beginRenameActiveSession(ctx);
1766
1897
  })
1767
1898
  .row()
1768
- .text("🗑 Unpair", async (ctx) => {
1769
- 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
+ });
1770
1903
  await this.showUnpairConfirmMenu(ctx);
1771
1904
  })
1772
1905
  .row()
1773
- .text("⬅ Back", async (ctx) => {
1774
- 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
+ });
1775
1910
  await this.showMainMenu(ctx);
1776
1911
  });
1777
1912
  }
@@ -1793,12 +1928,14 @@ class TelegramTransport {
1793
1928
  }
1794
1929
  createUnpairConfirmMenu() {
1795
1930
  return new menu_1.Menu("telegram-unpair-confirm-menu", this.createMenuOptions((ctx) => this.showUnpairConfirmMenu(ctx)))
1796
- .text("⚠ Confirm unpair", async (ctx) => {
1931
+ .text(async (ctx) => this.tForContext(ctx, "menu:settings.buttons.confirm_unpair"), async (ctx) => {
1797
1932
  await this.unpairActiveSession(ctx);
1798
1933
  })
1799
1934
  .row()
1800
- .text("⬅ Back", async (ctx) => {
1801
- 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
+ });
1802
1939
  await this.showSettingsMenu(ctx);
1803
1940
  });
1804
1941
  }
@@ -1822,9 +1959,9 @@ class TelegramTransport {
1822
1959
  const range = new menu_1.MenuRange();
1823
1960
  const principal = this.getPrincipalFromContext(ctx);
1824
1961
  if (!principal) {
1825
- range.text("No Telegram identity", async (innerCtx) => {
1962
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1826
1963
  await innerCtx.answerCallbackQuery({
1827
- text: "Telegram user or chat is missing.",
1964
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1828
1965
  show_alert: true,
1829
1966
  });
1830
1967
  });
@@ -1832,9 +1969,9 @@ class TelegramTransport {
1832
1969
  }
1833
1970
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1834
1971
  if (!sessionId) {
1835
- range.text("No active session", async (innerCtx) => {
1972
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1836
1973
  await innerCtx.answerCallbackQuery({
1837
- text: "No active session is linked yet.",
1974
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1838
1975
  show_alert: true,
1839
1976
  });
1840
1977
  });
@@ -1842,9 +1979,9 @@ class TelegramTransport {
1842
1979
  }
1843
1980
  const inboxMessages = await this.inboxStore.listInboxMessages(sessionId, 10);
1844
1981
  if (inboxMessages.length === 0) {
1845
- range.text("📭 Inbox is empty", async (innerCtx) => {
1982
+ range.text(await this.tForContext(ctx, "menu:inbox.labels.empty"), async (innerCtx) => {
1846
1983
  await innerCtx.answerCallbackQuery({
1847
- text: "No unsolicited Telegram messages are stored.",
1984
+ text: await this.tForContext(innerCtx, "menu:inbox.actions.empty"),
1848
1985
  show_alert: false,
1849
1986
  });
1850
1987
  });
@@ -1862,12 +1999,16 @@ class TelegramTransport {
1862
1999
  }
1863
2000
  return range;
1864
2001
  })
1865
- .text("🔄 Refresh", async (ctx) => {
1866
- 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
+ });
1867
2006
  await this.showInboxMenu(ctx);
1868
2007
  })
1869
- .text("⬅ Back", async (ctx) => {
1870
- 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
+ });
1871
2012
  await this.showMainMenu(ctx);
1872
2013
  });
1873
2014
  }
@@ -1880,9 +2021,9 @@ class TelegramTransport {
1880
2021
  const range = new menu_1.MenuRange();
1881
2022
  const principal = this.getPrincipalFromContext(ctx);
1882
2023
  if (!principal) {
1883
- range.text("No Telegram identity", async (innerCtx) => {
2024
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1884
2025
  await innerCtx.answerCallbackQuery({
1885
- text: "Telegram user or chat is missing.",
2026
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1886
2027
  show_alert: true,
1887
2028
  });
1888
2029
  });
@@ -1890,9 +2031,9 @@ class TelegramTransport {
1890
2031
  }
1891
2032
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1892
2033
  if (!sessionId) {
1893
- range.text("No active session", async (innerCtx) => {
2034
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1894
2035
  await innerCtx.answerCallbackQuery({
1895
- text: "No active session is linked yet.",
2036
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1896
2037
  show_alert: true,
1897
2038
  });
1898
2039
  });
@@ -1900,9 +2041,9 @@ class TelegramTransport {
1900
2041
  }
1901
2042
  const entries = await this.listActiveSessionStorageEntries(sessionId);
1902
2043
  if (entries.length === 0) {
1903
- range.text("📭 Storage is empty", async (innerCtx) => {
2044
+ range.text(await this.tForContext(ctx, "menu:storage.labels.empty"), async (innerCtx) => {
1904
2045
  await innerCtx.answerCallbackQuery({
1905
- text: .mcp-xchange пока нет файлов для этой сессии.",
2046
+ text: await this.tForContext(innerCtx, "menu:storage.actions.empty"),
1906
2047
  });
1907
2048
  });
1908
2049
  return range;
@@ -1919,12 +2060,16 @@ class TelegramTransport {
1919
2060
  }
1920
2061
  return range;
1921
2062
  })
1922
- .text("🔄 Refresh", async (ctx) => {
1923
- 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
+ });
1924
2067
  await this.showStorageMenu(ctx);
1925
2068
  })
1926
- .text("⬅ Back", async (ctx) => {
1927
- 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
+ });
1928
2073
  await this.showMainMenu(ctx);
1929
2074
  });
1930
2075
  }
@@ -1937,9 +2082,9 @@ class TelegramTransport {
1937
2082
  const range = new menu_1.MenuRange();
1938
2083
  const principal = this.getPrincipalFromContext(ctx);
1939
2084
  if (!principal) {
1940
- range.text("No Telegram identity", async (innerCtx) => {
2085
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1941
2086
  await innerCtx.answerCallbackQuery({
1942
- text: "Telegram user or chat is missing.",
2087
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
1943
2088
  show_alert: true,
1944
2089
  });
1945
2090
  });
@@ -1947,9 +2092,9 @@ class TelegramTransport {
1947
2092
  }
1948
2093
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
1949
2094
  if (!sessionId) {
1950
- range.text("No active session", async (innerCtx) => {
2095
+ range.text(await this.tForContext(ctx, "common:menu.no_active_session_label"), async (innerCtx) => {
1951
2096
  await innerCtx.answerCallbackQuery({
1952
- text: "No active session is linked yet.",
2097
+ text: await this.tForContext(innerCtx, "common:errors.no_active_session"),
1953
2098
  show_alert: true,
1954
2099
  });
1955
2100
  });
@@ -1957,9 +2102,9 @@ class TelegramTransport {
1957
2102
  }
1958
2103
  const filePaths = await this.listActiveSessionScreenshots(sessionId);
1959
2104
  if (filePaths.length === 0) {
1960
- range.text("📭 No screenshots", async (innerCtx) => {
2105
+ range.text(await this.tForContext(ctx, "menu:screenshots.labels.empty"), async (innerCtx) => {
1961
2106
  await innerCtx.answerCallbackQuery({
1962
- text: "No screenshots are stored for this session.",
2107
+ text: await this.tForContext(innerCtx, "menu:screenshots.actions.empty"),
1963
2108
  });
1964
2109
  });
1965
2110
  return range;
@@ -1976,12 +2121,16 @@ class TelegramTransport {
1976
2121
  }
1977
2122
  return range;
1978
2123
  })
1979
- .text("🔄 Refresh", async (ctx) => {
1980
- 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
+ });
1981
2128
  await this.showScreenshotsMenu(ctx);
1982
2129
  })
1983
- .text("⬅ Back", async (ctx) => {
1984
- 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
+ });
1985
2134
  await this.showBrowserMenu(ctx);
1986
2135
  });
1987
2136
  }
@@ -1995,9 +2144,9 @@ class TelegramTransport {
1995
2144
  try {
1996
2145
  const principal = this.getPrincipalFromContext(ctx);
1997
2146
  if (!principal) {
1998
- range.text("No Telegram identity", async (innerCtx) => {
2147
+ range.text(await this.tForContext(ctx, "common:menu.no_telegram_identity_label"), async (innerCtx) => {
1999
2148
  await innerCtx.answerCallbackQuery({
2000
- text: "Telegram user or chat is missing.",
2149
+ text: await this.tForContext(innerCtx, "common:errors.missing_telegram_context"),
2001
2150
  show_alert: true,
2002
2151
  });
2003
2152
  });
@@ -2006,9 +2155,9 @@ class TelegramTransport {
2006
2155
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2007
2156
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
2008
2157
  if (sessionIds.length === 0) {
2009
- range.text("🫥 No linked sessions", async (innerCtx) => {
2158
+ range.text(await this.tForContext(ctx, "menu:sessions.labels.no_linked_sessions"), async (innerCtx) => {
2010
2159
  await innerCtx.answerCallbackQuery({
2011
- text: "No linked sessions found for this Telegram identity.",
2160
+ text: await this.tForContext(innerCtx, "menu:sessions.actions.no_linked_sessions"),
2012
2161
  show_alert: true,
2013
2162
  });
2014
2163
  });
@@ -2046,21 +2195,25 @@ class TelegramTransport {
2046
2195
  userId: ctx.from?.id,
2047
2196
  error: error instanceof Error ? (error.stack ?? error.message) : String(error),
2048
2197
  });
2049
- range.text("⚠ Sessions unavailable", async (innerCtx) => {
2198
+ range.text(await this.tForContext(ctx, "menu:sessions.labels.unavailable"), async (innerCtx) => {
2050
2199
  await innerCtx.answerCallbackQuery({
2051
- text: "Sessions menu is temporarily unavailable.",
2200
+ text: await this.tForContext(innerCtx, "menu:sessions.actions.unavailable"),
2052
2201
  show_alert: true,
2053
2202
  });
2054
2203
  });
2055
2204
  return range;
2056
2205
  }
2057
2206
  })
2058
- .text("🔄 Refresh", async (ctx) => {
2059
- 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
+ });
2060
2211
  await this.showSessionsMenu(ctx);
2061
2212
  })
2062
- .text("🛠 Tools", async (ctx) => {
2063
- 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
+ });
2064
2217
  await this.showDeveloperMenu(ctx);
2065
2218
  });
2066
2219
  }
@@ -2070,13 +2223,15 @@ class TelegramTransport {
2070
2223
  ...this.createMenuOptions((ctx) => this.showInboxMenu(ctx)),
2071
2224
  })
2072
2225
  .text({
2073
- text: "🗑 Delete",
2226
+ text: async (ctx) => this.tForContext(ctx, "common:menu.delete"),
2074
2227
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2075
2228
  }, async (ctx) => {
2076
2229
  await this.handleInboxMessageDelete(ctx);
2077
2230
  })
2078
- .text("✖ Close", async (ctx) => {
2079
- 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
+ });
2080
2235
  await ctx.deleteMessage();
2081
2236
  });
2082
2237
  }
@@ -2086,20 +2241,22 @@ class TelegramTransport {
2086
2241
  ...this.createMenuOptions((ctx) => this.showStorageMenu(ctx)),
2087
2242
  })
2088
2243
  .text({
2089
- text: "📥 Получить",
2244
+ text: async (ctx) => this.tForContext(ctx, "common:menu.get"),
2090
2245
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2091
2246
  }, async (ctx) => {
2092
2247
  await this.handleStorageGet(ctx);
2093
2248
  })
2094
2249
  .text({
2095
- text: "🗑 Delete",
2250
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.delete"),
2096
2251
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2097
2252
  }, async (ctx) => {
2098
2253
  await this.handleStorageDelete(ctx);
2099
2254
  })
2100
2255
  .row()
2101
- .text("⬅ Back", async (ctx) => {
2102
- 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
+ });
2103
2260
  await this.showStorageMenu(ctx);
2104
2261
  });
2105
2262
  }
@@ -2109,20 +2266,22 @@ class TelegramTransport {
2109
2266
  ...this.createMenuOptions((ctx) => this.showScreenshotsMenu(ctx)),
2110
2267
  })
2111
2268
  .text({
2112
- text: "📥 Получить",
2269
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.get"),
2113
2270
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2114
2271
  }, async (ctx) => {
2115
2272
  await this.handleScreenshotGet(ctx);
2116
2273
  })
2117
2274
  .text({
2118
- text: "🗑 Delete",
2275
+ text: async (ctx) => this.tForContext(ctx, "menu:storage.buttons.delete"),
2119
2276
  payload: (ctx) => readMenuPayloadKey(ctx) ?? "missing",
2120
2277
  }, async (ctx) => {
2121
2278
  await this.handleScreenshotDelete(ctx);
2122
2279
  })
2123
2280
  .row()
2124
- .text("⬅ Back", async (ctx) => {
2125
- 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
+ });
2126
2285
  await this.showScreenshotsMenu(ctx);
2127
2286
  });
2128
2287
  }
@@ -2468,8 +2627,12 @@ class TelegramTransport {
2468
2627
  });
2469
2628
  }
2470
2629
  await this.replyText(ctx, currentTarget.projectUuid
2471
- ? `Файл отправлен в сессию ${currentTarget.targetSessionLabel}.`
2472
- : `Файл отправлен напарнику ${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
+ }), {
2473
2636
  kind: "inbox",
2474
2637
  sessionId,
2475
2638
  }, { reply_markup: this.mainMenu });
@@ -2493,11 +2656,21 @@ class TelegramTransport {
2493
2656
  });
2494
2657
  await this.replyText(ctx, session?.label
2495
2658
  ? attachments.length === 1
2496
- ? `Файл доставлен в сессию ${session.label}.`
2497
- : `Файлы доставлены в сессию ${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
+ })
2498
2666
  : attachments.length === 1
2499
- ? `Файл доставлен в сессию ${sessionId}.`
2500
- : `Файлы доставлены в сессию ${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
+ }), {
2501
2674
  kind: "inbox",
2502
2675
  sessionId,
2503
2676
  }, { reply_markup: this.mainMenu });
@@ -2533,6 +2706,12 @@ class TelegramTransport {
2533
2706
  : String(error),
2534
2707
  };
2535
2708
  if ((0, client_1.isTmuxUnavailableError)(error)) {
2709
+ void this.sessionStore.getSession(sessionId).then((session) => {
2710
+ if (!session?.tmuxTarget) {
2711
+ return;
2712
+ }
2713
+ return this.notifyTmuxUnavailable(sessionId, session, error);
2714
+ });
2536
2715
  this.logger.warn("tmux nudge skipped because tmux is unavailable", payload);
2537
2716
  return;
2538
2717
  }
@@ -2611,7 +2790,11 @@ class TelegramTransport {
2611
2790
  await this.sessionStore.setSession({
2612
2791
  ...session,
2613
2792
  tmuxTarget,
2614
- ...(tmuxTarget ? { tmuxPaneId: tmuxTarget } : {}),
2793
+ ...(tmuxTarget.startsWith("%")
2794
+ ? { tmuxPaneId: tmuxTarget }
2795
+ : session.tmuxPaneId
2796
+ ? { tmuxPaneId: session.tmuxPaneId }
2797
+ : {}),
2615
2798
  lastTmuxNudgeAt,
2616
2799
  });
2617
2800
  this.tmuxNudgeFailureNoticeAt.delete(sessionId);
@@ -2669,6 +2852,7 @@ class TelegramTransport {
2669
2852
  const sessionLabel = session.label ?? sessionId;
2670
2853
  const tmuxTarget = session.tmuxTarget ?? "unknown";
2671
2854
  const errorMessage = error instanceof Error ? error.message : String(error);
2855
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
2672
2856
  try {
2673
2857
  await this.sendNotification({
2674
2858
  sessionId,
@@ -2678,11 +2862,17 @@ class TelegramTransport {
2678
2862
  telegramUserId: binding.telegramUserId,
2679
2863
  },
2680
2864
  message: [
2681
- `⚠️ Автоматический tmux nudge для сессии ${sessionLabel} не сработал.`,
2682
- `Сохранённый tmux target больше недействителен: ${tmuxTarget}`,
2683
- `Ошибка: ${errorMessage}`,
2684
- "Обычно это значит, что pane/window/session был пересоздан.",
2685
- "Перепривяжи 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"),
2686
2876
  ].join("\n"),
2687
2877
  });
2688
2878
  }
@@ -2698,6 +2888,58 @@ class TelegramTransport {
2698
2888
  });
2699
2889
  }
2700
2890
  }
2891
+ async notifyTmuxUnavailable(sessionId, session, error) {
2892
+ const binding = await this.bindingStore.getBinding(sessionId);
2893
+ if (!binding) {
2894
+ return;
2895
+ }
2896
+ const nowMs = Date.now();
2897
+ const lastNoticeAt = this.tmuxNudgeFailureNoticeAt.get(sessionId);
2898
+ if (lastNoticeAt &&
2899
+ nowMs - lastNoticeAt < TMUX_NUDGE_FAILURE_NOTICE_COOLDOWN_MS) {
2900
+ return;
2901
+ }
2902
+ this.tmuxNudgeFailureNoticeAt.set(sessionId, nowMs);
2903
+ const sessionLabel = session.label ?? sessionId;
2904
+ const tmuxTarget = session.tmuxTarget ?? "unknown";
2905
+ const errorMessage = error instanceof Error ? error.message : String(error);
2906
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
2907
+ try {
2908
+ await this.sendNotification({
2909
+ sessionId,
2910
+ sessionLabel: "TellyMCP",
2911
+ recipient: {
2912
+ telegramChatId: binding.telegramChatId,
2913
+ telegramUserId: binding.telegramUserId,
2914
+ },
2915
+ message: [
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"),
2928
+ ].join("\n"),
2929
+ });
2930
+ }
2931
+ catch (notifyError) {
2932
+ this.logger.warn("Failed to deliver tmux unavailable notification", {
2933
+ sessionId,
2934
+ tmuxTarget,
2935
+ telegramChatId: binding.telegramChatId,
2936
+ telegramUserId: binding.telegramUserId,
2937
+ notifyError: notifyError instanceof Error
2938
+ ? (notifyError.stack ?? notifyError.message)
2939
+ : String(notifyError),
2940
+ });
2941
+ }
2942
+ }
2701
2943
  async sendTypingForSession(sessionId) {
2702
2944
  const binding = await this.bindingStore.getBinding(sessionId);
2703
2945
  if (!binding) {
@@ -2742,13 +2984,14 @@ class TelegramTransport {
2742
2984
  await this.renderMenuHtmlScreen(ctx, intro ? `${intro}\n\n${text}` : text, { kind: "menu" }, this.mainMenu);
2743
2985
  }
2744
2986
  async buildMainMenuText(ctx) {
2987
+ const locale = await this.resolveLocaleForContext(ctx);
2745
2988
  const principal = this.getPrincipalFromContext(ctx);
2746
2989
  if (!principal) {
2747
- return "Telegram identity is unavailable for this chat.";
2990
+ return this.t(locale, "common:errors.no_telegram_identity");
2748
2991
  }
2749
2992
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2750
2993
  if (!activeSessionId) {
2751
- return "No active session is linked yet. Pair a session via /start <code>.";
2994
+ return this.t(locale, "common:errors.no_active_session");
2752
2995
  }
2753
2996
  const session = await this.sessionStore.getSession(activeSessionId);
2754
2997
  const inboxCount = await this.inboxStore.countInboxMessages(activeSessionId);
@@ -2760,80 +3003,89 @@ class TelegramTransport {
2760
3003
  ? await this.sessionStore.getSession(session.linkedSessionId)
2761
3004
  : null;
2762
3005
  return [
2763
- `🎛 Session: ${sessionName}`,
3006
+ this.t(locale, "menu:main.screen.title", { sessionName }),
2764
3007
  "",
2765
- `📥 Inbox messages: ${inboxCount}`,
2766
- ...(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
+ : []),
2767
3012
  ...(session?.linkedSessionId
2768
3013
  ? [
2769
- `🤝 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
+ }),
2770
3017
  "",
2771
- "Share API details, what's new, errors, and git changes with your teammate.",
3018
+ this.t(locale, "menu:main.screen.partner_hint"),
2772
3019
  ]
2773
- : ["", "🔗 Link a partner session to coordinate through shared notes and files."]),
3020
+ : ["", this.t(locale, "menu:main.screen.link_hint")]),
2774
3021
  ].join("\n");
2775
3022
  }
2776
- async getTmuxStatusLine() {
2777
- return "🖧 TMUX mode: direct";
3023
+ async getTmuxStatusLine(locale) {
3024
+ return this.t(locale, "menu:main.screen.tmux_mode_direct");
2778
3025
  }
2779
3026
  async buildMainMenuFingerprint(ctx) {
3027
+ const locale = await this.resolveLocaleForContext(ctx);
2780
3028
  const principal = this.getPrincipalFromContext(ctx);
2781
3029
  if (!principal) {
2782
- return "no-principal";
3030
+ return `${locale}:no-principal`;
2783
3031
  }
2784
3032
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2785
3033
  if (!sessionId) {
2786
- return "no-active-session";
3034
+ return `${locale}:no-active-session`;
2787
3035
  }
2788
3036
  const count = await this.inboxStore.countInboxMessages(sessionId);
2789
3037
  const session = await this.sessionStore.getSession(sessionId);
2790
- return `${sessionId}:${count}:${session?.linkedSessionId ?? "none"}`;
3038
+ return `${locale}:${sessionId}:${count}:${session?.linkedSessionId ?? "none"}`;
2791
3039
  }
2792
3040
  async buildInboxFingerprint(ctx) {
3041
+ const locale = await this.resolveLocaleForContext(ctx);
2793
3042
  const principal = this.getPrincipalFromContext(ctx);
2794
3043
  if (!principal) {
2795
- return "no-principal";
3044
+ return `${locale}:no-principal`;
2796
3045
  }
2797
3046
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2798
3047
  if (!sessionId) {
2799
- return "no-active-session";
3048
+ return `${locale}:no-active-session`;
2800
3049
  }
2801
3050
  const messages = await this.inboxStore.listInboxMessages(sessionId, 10);
2802
- return `${sessionId}:${messages.map((message) => message.id).join(",")}`;
3051
+ return `${locale}:${sessionId}:${messages.map((message) => message.id).join(",")}`;
2803
3052
  }
2804
3053
  async buildStorageFingerprint(ctx) {
3054
+ const locale = await this.resolveLocaleForContext(ctx);
2805
3055
  const principal = this.getPrincipalFromContext(ctx);
2806
3056
  if (!principal) {
2807
- return "no-principal";
3057
+ return `${locale}:no-principal`;
2808
3058
  }
2809
3059
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2810
3060
  if (!sessionId) {
2811
- return "no-active-session";
3061
+ return `${locale}:no-active-session`;
2812
3062
  }
2813
3063
  const entries = await this.listActiveSessionStorageEntries(sessionId);
2814
- return `${sessionId}:${entries.map((entry) => entry.filePath).join(",")}`;
3064
+ return `${locale}:${sessionId}:${entries.map((entry) => entry.filePath).join(",")}`;
2815
3065
  }
2816
3066
  async buildScreenshotsFingerprint(ctx) {
3067
+ const locale = await this.resolveLocaleForContext(ctx);
2817
3068
  const principal = this.getPrincipalFromContext(ctx);
2818
3069
  if (!principal) {
2819
- return "no-principal";
3070
+ return `${locale}:no-principal`;
2820
3071
  }
2821
3072
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2822
3073
  if (!sessionId) {
2823
- return "no-active-session";
3074
+ return `${locale}:no-active-session`;
2824
3075
  }
2825
3076
  const files = await this.listActiveSessionScreenshots(sessionId);
2826
- return `${sessionId}:${files.join(",")}`;
3077
+ return `${locale}:${sessionId}:${files.join(",")}`;
2827
3078
  }
2828
3079
  async buildSessionsFingerprint(ctx) {
2829
3080
  try {
3081
+ const locale = await this.resolveLocaleForContext(ctx);
2830
3082
  const principal = this.getPrincipalFromContext(ctx);
2831
3083
  if (!principal) {
2832
- return "no-principal";
3084
+ return `${locale}:no-principal`;
2833
3085
  }
2834
3086
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2835
3087
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
2836
- return `${activeSessionId ?? "none"}:${sessionIds.join(",")}`;
3088
+ return `${locale}:${activeSessionId ?? "none"}:${sessionIds.join(",")}`;
2837
3089
  }
2838
3090
  catch (error) {
2839
3091
  this.logger.warn("Failed to build Telegram sessions menu fingerprint", {
@@ -2845,61 +3097,71 @@ class TelegramTransport {
2845
3097
  }
2846
3098
  }
2847
3099
  async buildLinkFingerprint(ctx) {
3100
+ const locale = await this.resolveLocaleForContext(ctx);
2848
3101
  const principal = this.getPrincipalFromContext(ctx);
2849
3102
  if (!principal) {
2850
- return "no-principal";
3103
+ return `${locale}:no-principal`;
2851
3104
  }
2852
3105
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2853
3106
  if (!activeSessionId) {
2854
- return "no-active-session";
3107
+ return `${locale}:no-active-session`;
2855
3108
  }
2856
3109
  const session = await this.sessionStore.getSession(activeSessionId);
2857
3110
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal))
2858
3111
  .filter((sessionId) => sessionId !== activeSessionId)
2859
3112
  .sort();
2860
- return `${activeSessionId}:${session?.linkedSessionId ?? "none"}:${sessionIds.join(",")}`;
3113
+ return `${locale}:${activeSessionId}:${session?.linkedSessionId ?? "none"}:${sessionIds.join(",")}`;
2861
3114
  }
2862
3115
  async buildInboxButtonLabel(ctx) {
3116
+ const locale = await this.resolveLocaleForContext(ctx);
2863
3117
  const principal = this.getPrincipalFromContext(ctx);
2864
3118
  if (!principal) {
2865
- return "Inbox";
3119
+ return this.t(locale, "menu:inbox.button");
2866
3120
  }
2867
3121
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2868
3122
  if (!sessionId) {
2869
- return "📥 Inbox";
3123
+ return this.t(locale, "menu:inbox.button");
2870
3124
  }
2871
3125
  const count = await this.inboxStore.countInboxMessages(sessionId);
2872
- 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");
2873
3129
  }
2874
3130
  async buildScreenshotsButtonLabel(ctx) {
3131
+ const locale = await this.resolveLocaleForContext(ctx);
2875
3132
  const principal = this.getPrincipalFromContext(ctx);
2876
3133
  if (!principal) {
2877
- return "📸 Screenshots";
3134
+ return this.t(locale, "menu:browser.buttons.screenshots");
2878
3135
  }
2879
3136
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2880
3137
  if (!sessionId) {
2881
- return "📸 Screenshots";
3138
+ return this.t(locale, "menu:browser.buttons.screenshots");
2882
3139
  }
2883
3140
  const count = (await this.listActiveSessionScreenshots(sessionId)).length;
2884
- 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");
2885
3144
  }
2886
3145
  async buildLinkButtonLabel(ctx) {
3146
+ const locale = await this.resolveLocaleForContext(ctx);
2887
3147
  const principal = this.getPrincipalFromContext(ctx);
2888
3148
  if (!principal) {
2889
- return "🔗 Связать";
3149
+ return this.t(locale, "menu:local.buttons.link");
2890
3150
  }
2891
3151
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
2892
3152
  if (!sessionId) {
2893
- return "🔗 Связать";
3153
+ return this.t(locale, "menu:local.buttons.link");
2894
3154
  }
2895
3155
  const session = await this.sessionStore.getSession(sessionId);
2896
3156
  if (!session?.linkedSessionId) {
2897
- return "🔗 Связать";
3157
+ return this.t(locale, "menu:local.buttons.link");
2898
3158
  }
2899
3159
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
2900
3160
  return linkedSession?.label
2901
- ? `🔓 Разорвать ${linkedSession.label}`
2902
- : "🔓 Разорвать";
3161
+ ? this.t(locale, "menu:link.buttons.unlink_with_name", {
3162
+ sessionName: linkedSession.label,
3163
+ })
3164
+ : this.t(locale, "menu:link.buttons.unlink");
2903
3165
  }
2904
3166
  async createInboxMenuPayload(sessionId, messageId) {
2905
3167
  const key = (0, ids_1.createMenuPayloadKey)();
@@ -3137,20 +3399,28 @@ class TelegramTransport {
3137
3399
  (meta?.relativePath ? node_path_1.default.basename(meta.relativePath) : undefined) ||
3138
3400
  node_path_1.default.basename(input.filePath);
3139
3401
  const principalKey = buildPrincipalKey(principal);
3402
+ const locale = await this.resolveLocaleForContext(ctx);
3140
3403
  await ctx.answerCallbackQuery({
3141
- text: "Project file handoff.",
3404
+ text: this.t(locale, "menu:handoff.prompt_title"),
3142
3405
  });
3143
3406
  const sent = await this.replyText(ctx, [
3144
- "🤝 Передать участнику",
3407
+ this.t(locale, "menu:handoff.prompt_title"),
3145
3408
  "",
3146
- `Сессия: ${session?.label ?? input.sessionId} -> ${input.targetSessionLabel}`,
3147
- `Кому: ${input.targetSessionLabel}`,
3148
- `Файл: ${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
+ }),
3149
3419
  "",
3150
- "Отправь следующим сообщением описание или инструкции для этого файла.",
3151
- "Этот текст будет приложен к handoff.",
3420
+ this.t(locale, "menu:handoff.prompt_body"),
3421
+ this.t(locale, "menu:handoff.prompt_hint"),
3152
3422
  ].join("\n"), { kind: "menu", sessionId: input.sessionId }, {
3153
- 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"),
3154
3424
  });
3155
3425
  this.pendingFileHandoffs.set(principalKey, {
3156
3426
  sessionId: input.sessionId,
@@ -3164,10 +3434,11 @@ class TelegramTransport {
3164
3434
  });
3165
3435
  }
3166
3436
  async handleLinkButton(ctx) {
3437
+ const locale = await this.resolveLocaleForContext(ctx);
3167
3438
  const principal = this.getPrincipalFromContext(ctx);
3168
3439
  if (!principal) {
3169
3440
  await ctx.answerCallbackQuery({
3170
- text: "Telegram identity недоступна.",
3441
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3171
3442
  show_alert: true,
3172
3443
  });
3173
3444
  return;
@@ -3175,7 +3446,7 @@ class TelegramTransport {
3175
3446
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3176
3447
  if (!sessionId) {
3177
3448
  await ctx.answerCallbackQuery({
3178
- text: "Активная сессия не выбрана.",
3449
+ text: await this.tForContext(ctx, "common:errors.no_active_session"),
3179
3450
  show_alert: true,
3180
3451
  });
3181
3452
  return;
@@ -3183,18 +3454,23 @@ class TelegramTransport {
3183
3454
  const session = await this.sessionStore.getSession(sessionId);
3184
3455
  if (session?.linkedSessionId) {
3185
3456
  await this.unlinkSessions(sessionId, session.linkedSessionId);
3186
- await ctx.answerCallbackQuery({ text: "Partner session unlinked." });
3187
- 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"));
3188
3461
  return;
3189
3462
  }
3190
- await ctx.answerCallbackQuery({ text: "Choose a partner session." });
3463
+ await ctx.answerCallbackQuery({
3464
+ text: this.t(locale, "menu:link.actions.choose_partner"),
3465
+ });
3191
3466
  await this.showLinkMenu(ctx);
3192
3467
  }
3193
3468
  async showPartnerEntryPoint(ctx) {
3469
+ const locale = await this.resolveLocaleForContext(ctx);
3194
3470
  const principal = this.getPrincipalFromContext(ctx);
3195
3471
  if (!principal) {
3196
3472
  await ctx.answerCallbackQuery({
3197
- text: "Идентификатор Telegram недоступен.",
3473
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3198
3474
  show_alert: true,
3199
3475
  });
3200
3476
  return;
@@ -3202,7 +3478,7 @@ class TelegramTransport {
3202
3478
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3203
3479
  if (!sessionId) {
3204
3480
  await ctx.answerCallbackQuery({
3205
- text: "Активная сессия не выбрана.",
3481
+ text: this.t(locale, "common:errors.no_active_session"),
3206
3482
  show_alert: true,
3207
3483
  });
3208
3484
  return;
@@ -3210,19 +3486,22 @@ class TelegramTransport {
3210
3486
  const session = await this.sessionStore.getSession(sessionId);
3211
3487
  if (!session?.linkedSessionId) {
3212
3488
  await ctx.answerCallbackQuery({
3213
- text: "Сначала свяжи сессию с напарником.",
3489
+ text: await this.tForContext(ctx, "menu:partner.screen.use_link_first"),
3214
3490
  show_alert: true,
3215
3491
  });
3216
3492
  return;
3217
3493
  }
3218
- await ctx.answerCallbackQuery({ text: "Открываю меню напарника." });
3494
+ await ctx.answerCallbackQuery({
3495
+ text: await this.tForContext(ctx, "menu:partner.actions.open_partner_menu"),
3496
+ });
3219
3497
  await this.showPartnerMenu(ctx);
3220
3498
  }
3221
3499
  async showPartnerFiles(ctx) {
3500
+ const locale = await this.resolveLocaleForContext(ctx);
3222
3501
  const principal = this.getPrincipalFromContext(ctx);
3223
3502
  if (!principal) {
3224
3503
  await ctx.answerCallbackQuery({
3225
- text: "Идентификатор Telegram недоступен.",
3504
+ text: this.t(locale, "common:errors.no_telegram_identity"),
3226
3505
  show_alert: true,
3227
3506
  });
3228
3507
  return;
@@ -3230,7 +3509,7 @@ class TelegramTransport {
3230
3509
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
3231
3510
  if (!sessionId) {
3232
3511
  await ctx.answerCallbackQuery({
3233
- text: "Активная сессия не выбрана.",
3512
+ text: this.t(locale, "common:errors.no_active_session"),
3234
3513
  show_alert: true,
3235
3514
  });
3236
3515
  return;
@@ -3238,7 +3517,7 @@ class TelegramTransport {
3238
3517
  const session = await this.sessionStore.getSession(sessionId);
3239
3518
  if (!session?.linkedSessionId) {
3240
3519
  await ctx.answerCallbackQuery({
3241
- text: "Сначала свяжи сессию с напарником.",
3520
+ text: this.t(locale, "menu:partner.screen.use_link_first"),
3242
3521
  show_alert: true,
3243
3522
  });
3244
3523
  return;
@@ -3246,13 +3525,15 @@ class TelegramTransport {
3246
3525
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
3247
3526
  const files = await this.listActiveSessionFiles(sessionId);
3248
3527
  const lines = [
3249
- "📎 Выбор файла",
3528
+ this.t(locale, "menu:handoff.choose_title"),
3250
3529
  "",
3251
- `Получатель: ${linkedSession?.label ?? session.linkedSessionId}`,
3530
+ this.t(locale, "menu:handoff.choose_recipient", {
3531
+ label: linkedSession?.label ?? session.linkedSessionId,
3532
+ }),
3252
3533
  "",
3253
3534
  files.length > 0
3254
- ? "Выбери файл для отправки локальному напарнику."
3255
- : этой сессии нет загруженных файлов.",
3535
+ ? this.t(locale, "menu:handoff.choose_local")
3536
+ : this.t(locale, "menu:handoff.no_files"),
3256
3537
  ];
3257
3538
  const keyboard = new grammy_1.InlineKeyboard();
3258
3539
  for (const filePath of files) {
@@ -3261,7 +3542,7 @@ class TelegramTransport {
3261
3542
  const payloadKey = await this.createPartnerFileTargetPayload(sessionId, session.linkedSessionId, linkedSession?.label ?? session.linkedSessionId, filePath);
3262
3543
  keyboard.text(label, `partner-file-open:${payloadKey}`).row();
3263
3544
  }
3264
- keyboard.text("⬅ К напарнику", "partner-back");
3545
+ keyboard.text(await this.tForContext(ctx, "common:menu.back"), "partner-back");
3265
3546
  const text = lines.join("\n");
3266
3547
  if (ctx.callbackQuery?.message) {
3267
3548
  await this.editText(ctx, text, { kind: "menu", sessionId }, { reply_markup: keyboard });
@@ -3270,18 +3551,22 @@ class TelegramTransport {
3270
3551
  await this.replyText(ctx, text, { kind: "menu", sessionId }, { reply_markup: keyboard });
3271
3552
  }
3272
3553
  async showLocalEntryPoint(ctx) {
3273
- await ctx.answerCallbackQuery({ text: "Открываю локальное взаимодействие." });
3554
+ await ctx.answerCallbackQuery({
3555
+ text: await this.tForContext(ctx, "menu:local.actions.open_local"),
3556
+ });
3274
3557
  await this.showLocalMenu(ctx);
3275
3558
  }
3276
3559
  async showProjectsEntryPoint(ctx) {
3277
3560
  if (!this.config.distributed.gatewayPublicUrl) {
3278
3561
  await ctx.answerCallbackQuery({
3279
- text: "Collab доступен только через gateway.",
3562
+ text: await this.tForContext(ctx, "menu:collab.actions.gateway_only"),
3280
3563
  show_alert: true,
3281
3564
  });
3282
3565
  return;
3283
3566
  }
3284
- await ctx.answerCallbackQuery({ text: "Открываю Collab." });
3567
+ await ctx.answerCallbackQuery({
3568
+ text: await this.tForContext(ctx, "menu:collab.actions.open_collab"),
3569
+ });
3285
3570
  await this.showProjectsMenu(ctx);
3286
3571
  }
3287
3572
  async handleLinkTargetSelect(ctx) {
@@ -3821,6 +4106,47 @@ class TelegramTransport {
3821
4106
  telegramUserId: userId,
3822
4107
  };
3823
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
+ }
3824
4150
  getGatewayActorFromContext(ctx) {
3825
4151
  const firstName = ctx.from?.first_name?.trim();
3826
4152
  const lastName = ctx.from?.last_name?.trim();
@@ -3849,7 +4175,7 @@ class TelegramTransport {
3849
4175
  userId: ctx.from?.id,
3850
4176
  error: error instanceof Error ? (error.stack ?? error.message) : String(error),
3851
4177
  });
3852
- 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" });
3853
4179
  }
3854
4180
  }
3855
4181
  async showInboxMenu(ctx, introText) {
@@ -3909,15 +4235,18 @@ class TelegramTransport {
3909
4235
  await this.renderMenuScreen(ctx, introText ? `${introText}\n\n${text}` : text, { kind: "menu" }, this.collabToolsMenu);
3910
4236
  }
3911
4237
  buildCollabHistoryMarkdown(input) {
4238
+ const locale = input.locale ?? "en";
3912
4239
  const lines = [
3913
- "# Collab History",
4240
+ this.t(locale, "menu:history.title"),
3914
4241
  "",
3915
- `Session: ${input.sessionLabel}`,
4242
+ this.t(locale, "menu:history.session", {
4243
+ sessionName: input.sessionLabel,
4244
+ }),
3916
4245
  `Generated at: ${new Date().toISOString()}`,
3917
4246
  "",
3918
4247
  ];
3919
4248
  if (input.history.length === 0) {
3920
- lines.push("No recent Collab events.");
4249
+ lines.push(this.t(locale, "menu:history.empty"));
3921
4250
  lines.push("");
3922
4251
  return lines.join("\n");
3923
4252
  }
@@ -3929,7 +4258,9 @@ class TelegramTransport {
3929
4258
  lines.push(`- Direction: ${item.direction}`);
3930
4259
  lines.push(`- Route: ${item.from_label} -> ${item.to_label}`);
3931
4260
  if (item.project_name) {
3932
- lines.push(`- Project: ${item.project_name}`);
4261
+ lines.push(this.t(locale, "menu:history.project", {
4262
+ projectName: item.project_name,
4263
+ }));
3933
4264
  }
3934
4265
  if (item.delivery_status) {
3935
4266
  lines.push(`- Status: ${item.delivery_status}`);
@@ -3940,27 +4271,33 @@ class TelegramTransport {
3940
4271
  return lines.join("\n");
3941
4272
  }
3942
4273
  async handleCollabHistoryExport(ctx) {
4274
+ const locale = await this.resolveLocaleForContext(ctx);
3943
4275
  const { principal, session } = await this.loadProjectsContext(ctx);
3944
4276
  if (!this.config.distributed.gatewayPublicUrl || !principal || !session) {
3945
4277
  await ctx.answerCallbackQuery({
3946
- text: "История Collab недоступна для этой сессии.",
4278
+ text: this.t(locale, "menu:collab.screen.unavailable"),
3947
4279
  show_alert: true,
3948
4280
  });
3949
4281
  return;
3950
4282
  }
3951
4283
  const history = await this.listGatewaySessionHistory(principal, session.sessionId);
3952
4284
  const markdown = this.buildCollabHistoryMarkdown({
4285
+ locale,
3953
4286
  sessionLabel: session.label ?? session.sessionId,
3954
4287
  history,
3955
4288
  });
3956
4289
  const fileName = `collab-history-${slugifyFilenamePart(session.label ?? session.sessionId) || "session"}.md`;
3957
4290
  await this.replyDocumentWithRetry(ctx, new grammy_1.InputFile(Buffer.from(markdown, "utf8"), fileName), {
3958
- caption: `Collab history for ${session.label ?? session.sessionId}`,
4291
+ caption: this.t(locale, "menu:history.caption", {
4292
+ sessionName: session.label ?? session.sessionId,
4293
+ }),
3959
4294
  }, {
3960
4295
  kind: "menu",
3961
4296
  sessionId: session.sessionId,
3962
4297
  });
3963
- await ctx.answerCallbackQuery({ text: "История отправлена в Telegram." });
4298
+ await ctx.answerCallbackQuery({
4299
+ text: this.t(locale, "menu:collab.buttons.history"),
4300
+ });
3964
4301
  }
3965
4302
  async showCollabDeleteMenu(ctx, introText) {
3966
4303
  const text = await this.buildCollabDeleteMenuText(ctx);
@@ -4024,24 +4361,26 @@ class TelegramTransport {
4024
4361
  });
4025
4362
  }
4026
4363
  async showHelp(ctx) {
4364
+ const locale = await this.resolveLocaleForContext(ctx);
4027
4365
  await this.replyText(ctx, [
4028
- "❓ Telegram MCP help",
4366
+ this.t(locale, "menu:help.title"),
4029
4367
  "",
4030
- "/menu - open the sessions list",
4031
- "/help - show this help",
4368
+ this.t(locale, "menu:help.menu"),
4369
+ this.t(locale, "menu:help.help"),
4032
4370
  "",
4033
- "How it works:",
4034
- "- choose the active session",
4035
- "- ordinary Telegram messages go to that session inbox",
4036
- "- if a tmux target is configured, the service nudges the agent automatically",
4037
- "- 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"),
4038
4376
  ].join("\n"), { kind: "menu" });
4039
4377
  }
4040
4378
  async showLiveViewLauncher(ctx) {
4379
+ const locale = await this.resolveLocaleForContext(ctx);
4041
4380
  const principal = this.getPrincipalFromContext(ctx);
4042
4381
  if (!principal) {
4043
4382
  await ctx.answerCallbackQuery({
4044
- text: "Telegram identity is unavailable.",
4383
+ text: this.t(locale, "menu:live.errors.identity_unavailable"),
4045
4384
  show_alert: true,
4046
4385
  });
4047
4386
  return;
@@ -4049,7 +4388,7 @@ class TelegramTransport {
4049
4388
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4050
4389
  if (!activeSessionId) {
4051
4390
  await ctx.answerCallbackQuery({
4052
- text: "No active session selected.",
4391
+ text: this.t(locale, "menu:live.errors.no_active_session"),
4053
4392
  show_alert: true,
4054
4393
  });
4055
4394
  return;
@@ -4058,7 +4397,7 @@ class TelegramTransport {
4058
4397
  (!this.config.webapp.publicUrl &&
4059
4398
  !this.config.distributed.gatewayPublicUrl)) {
4060
4399
  await ctx.answerCallbackQuery({
4061
- text: "WebApp is not enabled on the server.",
4400
+ text: this.t(locale, "menu:live.errors.webapp_disabled"),
4062
4401
  show_alert: true,
4063
4402
  });
4064
4403
  return;
@@ -4075,7 +4414,7 @@ class TelegramTransport {
4075
4414
  : resolveWebAppPublicBaseUrl(this.config);
4076
4415
  if (!baseUrl) {
4077
4416
  await ctx.answerCallbackQuery({
4078
- text: "WebApp public URL is not configured.",
4417
+ text: this.t(locale, "menu:live.errors.public_url_missing"),
4079
4418
  show_alert: true,
4080
4419
  });
4081
4420
  return;
@@ -4084,9 +4423,22 @@ class TelegramTransport {
4084
4423
  ? (0, relay_1.buildLiveRelaySessionId)(clientUuid, activeSessionId)
4085
4424
  : activeSessionId;
4086
4425
  const url = new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`);
4087
- await ctx.answerCallbackQuery({ text: "Открываю Live View." });
4088
- const sent = await this.replyText(ctx, `🖥 Live: ${session?.label ?? activeSessionId}`, { kind: "menu", sessionId: activeSessionId }, {
4089
- reply_markup: new grammy_1.InlineKeyboard().webApp("Open Live View", url.toString()),
4426
+ url.searchParams.set("launchMode", this.config.webapp.launchMode);
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 }, {
4437
+ reply_markup: this.buildLiveViewLaunchKeyboard((mode) => {
4438
+ const modeUrl = new URL(url.toString());
4439
+ modeUrl.searchParams.set("launchMode", mode);
4440
+ return modeUrl.toString();
4441
+ }, locale),
4090
4442
  });
4091
4443
  this.webAppLaunchRegistry.set(principal.telegramUserId, activeSessionId, this.config.webapp.initDataTtlSeconds, {
4092
4444
  telegramChatId: principal.telegramChatId,
@@ -4113,7 +4465,37 @@ class TelegramTransport {
4113
4465
  const liveSessionId = canUseRelay
4114
4466
  ? (0, relay_1.buildLiveRelaySessionId)(input.targetClientUuid, input.targetLocalSessionId, input.sourceClientUuid)
4115
4467
  : (input.targetLocalSessionId ?? input.targetSessionId);
4116
- return new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`).toString();
4468
+ const url = new URL(`${baseUrl}/live/${encodeURIComponent(liveSessionId)}`);
4469
+ url.searchParams.set("launchMode", input.launchMode ?? this.config.webapp.launchMode);
4470
+ return url.toString();
4471
+ }
4472
+ buildLiveViewLaunchKeyboard(getUrl, locale = "en") {
4473
+ const keyboard = new grammy_1.InlineKeyboard();
4474
+ const modes = [
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
+ },
4487
+ ];
4488
+ for (const [index, { mode, label }] of modes.entries()) {
4489
+ const url = getUrl(mode);
4490
+ if (!url) {
4491
+ continue;
4492
+ }
4493
+ keyboard.webApp(label, url);
4494
+ if (index === 1) {
4495
+ keyboard.row();
4496
+ }
4497
+ }
4498
+ return keyboard;
4117
4499
  }
4118
4500
  clearPendingInteractionsForContext(ctx) {
4119
4501
  const principal = this.getPrincipalFromContext(ctx);
@@ -4140,10 +4522,11 @@ class TelegramTransport {
4140
4522
  this.currentAttachmentTargets.delete(key);
4141
4523
  }
4142
4524
  async sendActiveSessionBuffer(ctx, scope) {
4525
+ const locale = await this.resolveLocaleForContext(ctx);
4143
4526
  const principal = this.getPrincipalFromContext(ctx);
4144
4527
  if (!principal) {
4145
4528
  await ctx.answerCallbackQuery({
4146
- text: "Telegram identity is unavailable.",
4529
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4147
4530
  show_alert: true,
4148
4531
  });
4149
4532
  return;
@@ -4151,7 +4534,7 @@ class TelegramTransport {
4151
4534
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4152
4535
  if (!sessionId) {
4153
4536
  await ctx.answerCallbackQuery({
4154
- text: "No active session selected.",
4537
+ text: this.t(locale, "common:errors.no_active_session"),
4155
4538
  show_alert: true,
4156
4539
  });
4157
4540
  return;
@@ -4202,14 +4585,15 @@ class TelegramTransport {
4202
4585
  }
4203
4586
  }
4204
4587
  async buildSessionsMenuText(ctx) {
4588
+ const locale = await this.resolveLocaleForContext(ctx);
4205
4589
  const principal = this.getPrincipalFromContext(ctx);
4206
4590
  if (!principal) {
4207
- return "Telegram identity is unavailable for this chat.";
4591
+ return this.t(locale, "common:errors.no_telegram_identity");
4208
4592
  }
4209
4593
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4210
4594
  const sessionIds = (await this.bindingStore.listBoundSessionIdsForPrincipal(principal)).sort();
4211
4595
  if (sessionIds.length === 0) {
4212
- return "No linked sessions found for this Telegram identity.";
4596
+ return this.t(locale, "menu:sessions.screen.no_linked_sessions");
4213
4597
  }
4214
4598
  let lastWorkedSession;
4215
4599
  for (const sessionId of sessionIds) {
@@ -4228,353 +4612,439 @@ class TelegramTransport {
4228
4612
  };
4229
4613
  }
4230
4614
  }
4231
- const lines = ["🗂 Choose active session", ""];
4615
+ const lines = [this.t(locale, "menu:sessions.screen.title"), ""];
4232
4616
  if (lastWorkedSession) {
4233
- 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
+ }));
4234
4620
  const formattedUpdatedAt = formatMenuTimestamp(lastWorkedSession.updatedAt);
4235
4621
  if (formattedUpdatedAt) {
4236
- lines.push(`⏱ Updated: <i>${escapeHtml(formattedUpdatedAt)}</i>`);
4622
+ lines.push(this.t(locale, "menu:sessions.screen.updated", {
4623
+ timestamp: escapeHtml(formattedUpdatedAt),
4624
+ }));
4237
4625
  }
4238
4626
  lines.push("");
4239
4627
  }
4240
4628
  if (activeSessionId) {
4241
4629
  const activeSession = await this.sessionStore.getSession(activeSessionId);
4242
- 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
+ }));
4243
4633
  lines.push("");
4244
4634
  }
4245
- lines.push(`<i>${escapeHtml(await this.getTmuxStatusLine())}</i>`);
4635
+ lines.push(`<i>${escapeHtml(await this.getTmuxStatusLine(locale))}</i>`);
4246
4636
  lines.push("");
4247
4637
  return lines.join("\n");
4248
4638
  }
4249
4639
  async buildInboxMenuText(ctx) {
4640
+ const locale = await this.resolveLocaleForContext(ctx);
4250
4641
  const principal = this.getPrincipalFromContext(ctx);
4251
4642
  if (!principal) {
4252
- return "Telegram identity is unavailable for this chat.";
4643
+ return this.t(locale, "common:errors.no_telegram_identity");
4253
4644
  }
4254
4645
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4255
4646
  if (!activeSessionId) {
4256
- return "No active session is linked yet. Pair a session via /start <code>.";
4647
+ return this.t(locale, "common:errors.no_active_session");
4257
4648
  }
4258
4649
  const session = await this.sessionStore.getSession(activeSessionId);
4259
4650
  const total = await this.inboxStore.countInboxMessages(activeSessionId);
4260
4651
  return [
4261
- "📥 Inbox",
4652
+ this.t(locale, "menu:inbox.screen.title"),
4262
4653
  "",
4263
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4264
- `📨 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
+ }),
4265
4660
  "",
4266
4661
  total > 0
4267
- ? "Choose a message below to inspect or delete it."
4268
- : "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"),
4269
4664
  ].join("\n");
4270
4665
  }
4271
4666
  async buildBufferMenuText(ctx) {
4667
+ const locale = await this.resolveLocaleForContext(ctx);
4272
4668
  const principal = this.getPrincipalFromContext(ctx);
4273
4669
  if (!principal) {
4274
- return "Telegram identity is unavailable for this chat.";
4670
+ return this.t(locale, "common:errors.no_telegram_identity");
4275
4671
  }
4276
4672
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4277
4673
  if (!activeSessionId) {
4278
- return "No active session is linked yet. Pair a session via /start <code>.";
4674
+ return this.t(locale, "common:errors.no_active_session");
4279
4675
  }
4280
4676
  const session = await this.sessionStore.getSession(activeSessionId);
4281
4677
  return [
4282
- "📄 Content",
4678
+ this.t(locale, "menu:buffer.screen.title"),
4283
4679
  "",
4284
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4285
- `🖥 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
+ }),
4286
4686
  "",
4287
- "Choose how much pane history to export as a Markdown file.",
4288
- "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"),
4289
4689
  ].join("\n");
4290
4690
  }
4291
4691
  async buildBrowserMenuText(ctx) {
4692
+ const locale = await this.resolveLocaleForContext(ctx);
4292
4693
  const principal = this.getPrincipalFromContext(ctx);
4293
4694
  if (!principal) {
4294
- return "Telegram identity is unavailable for this chat.";
4695
+ return this.t(locale, "common:errors.no_telegram_identity");
4295
4696
  }
4296
4697
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4297
4698
  if (!activeSessionId) {
4298
- return "No active session is linked yet. Pair a session via /start <code>.";
4699
+ return this.t(locale, "common:errors.no_active_session");
4299
4700
  }
4300
4701
  const session = await this.sessionStore.getSession(activeSessionId);
4301
4702
  const screenshots = await this.listActiveSessionScreenshots(activeSessionId);
4302
4703
  return [
4303
- "🌐 Browser",
4704
+ this.t(locale, "menu:browser.screen.title"),
4304
4705
  "",
4305
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4306
- `📸 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
+ }),
4307
4712
  "",
4308
- "Choose a browser-related action below.",
4713
+ this.t(locale, "menu:browser.screen.choose_action"),
4309
4714
  ].join("\n");
4310
4715
  }
4311
4716
  async buildSettingsMenuText(ctx) {
4717
+ const locale = await this.resolveLocaleForContext(ctx);
4312
4718
  const principal = this.getPrincipalFromContext(ctx);
4313
4719
  if (!principal) {
4314
- return "Telegram identity is unavailable for this chat.";
4720
+ return this.t(locale, "common:errors.no_telegram_identity");
4315
4721
  }
4316
4722
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4317
4723
  if (!activeSessionId) {
4318
- return "No active session is linked yet. Pair a session via /start <code>.";
4724
+ return this.t(locale, "common:errors.no_active_session");
4319
4725
  }
4320
4726
  const session = await this.sessionStore.getSession(activeSessionId);
4321
4727
  return [
4322
- "⚙ Settings",
4728
+ this.t(locale, "menu:settings.screen.title"),
4323
4729
  "",
4324
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4730
+ this.t(locale, "menu:settings.screen.active_session", {
4731
+ sessionName: session?.label ?? activeSessionId,
4732
+ }),
4325
4733
  "",
4326
- "Открой информацию о сессии, переименуй её или отвяжи от Telegram.",
4734
+ this.t(locale, "menu:settings.screen.hint"),
4327
4735
  ].join("\n");
4328
4736
  }
4329
4737
  async buildScreenshotsMenuText(ctx) {
4738
+ const locale = await this.resolveLocaleForContext(ctx);
4330
4739
  const principal = this.getPrincipalFromContext(ctx);
4331
4740
  if (!principal) {
4332
- return "Telegram identity is unavailable for this chat.";
4741
+ return this.t(locale, "common:errors.no_telegram_identity");
4333
4742
  }
4334
4743
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4335
4744
  if (!activeSessionId) {
4336
- return "No active session is linked yet. Pair a session via /start <code>.";
4745
+ return this.t(locale, "common:errors.no_active_session");
4337
4746
  }
4338
4747
  const session = await this.sessionStore.getSession(activeSessionId);
4339
4748
  const files = await this.listActiveSessionScreenshots(activeSessionId);
4340
4749
  return [
4341
- "📸 Screenshots",
4750
+ this.t(locale, "menu:screenshots.screen.title"),
4342
4751
  "",
4343
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4344
- `📦 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
+ }),
4345
4758
  "",
4346
4759
  files.length > 0
4347
- ? "Choose a screenshot below to get it in Telegram or delete it."
4348
- : "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"),
4349
4762
  ].join("\n");
4350
4763
  }
4351
4764
  async buildStorageMenuText(ctx) {
4765
+ const locale = await this.resolveLocaleForContext(ctx);
4352
4766
  const principal = this.getPrincipalFromContext(ctx);
4353
4767
  if (!principal) {
4354
- return "Telegram identity is unavailable for this chat.";
4768
+ return this.t(locale, "common:errors.no_telegram_identity");
4355
4769
  }
4356
4770
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4357
4771
  if (!activeSessionId) {
4358
- return "No active session is linked yet. Pair a session via /start <code>.";
4772
+ return this.t(locale, "common:errors.no_active_session");
4359
4773
  }
4360
4774
  const session = await this.sessionStore.getSession(activeSessionId);
4361
4775
  const entries = await this.listActiveSessionStorageEntries(activeSessionId);
4362
4776
  return [
4363
- "📦 Storage",
4777
+ this.t(locale, "menu:storage.screen.title"),
4364
4778
  "",
4365
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4366
- `📦 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
+ }),
4367
4785
  "",
4368
4786
  entries.length > 0
4369
- ? "Choose a file below to inspect it or send it to Telegram."
4370
- : "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"),
4371
4789
  ].join("\n");
4372
4790
  }
4373
4791
  async buildLinkMenuText(ctx) {
4792
+ const locale = await this.resolveLocaleForContext(ctx);
4374
4793
  const principal = this.getPrincipalFromContext(ctx);
4375
4794
  if (!principal) {
4376
- return "Telegram identity is unavailable for this chat.";
4795
+ return this.t(locale, "common:errors.no_telegram_identity");
4377
4796
  }
4378
4797
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4379
4798
  if (!activeSessionId) {
4380
- return "No active session is linked yet. Pair a session via /start <code>.";
4799
+ return this.t(locale, "common:errors.no_active_session");
4381
4800
  }
4382
4801
  const session = await this.sessionStore.getSession(activeSessionId);
4383
4802
  return [
4384
- "🔗 Link partner",
4803
+ this.t(locale, "menu:link.screen.title"),
4385
4804
  "",
4386
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4805
+ this.t(locale, "menu:link.screen.active_session", {
4806
+ sessionName: session?.label ?? activeSessionId,
4807
+ }),
4387
4808
  "",
4388
- "Choose another session to link as a teammate.",
4389
- "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"),
4390
4811
  ].join("\n");
4391
4812
  }
4392
4813
  async buildPartnerMenuText(ctx) {
4814
+ const locale = await this.resolveLocaleForContext(ctx);
4393
4815
  const principal = this.getPrincipalFromContext(ctx);
4394
4816
  if (!principal) {
4395
- return "Telegram identity is unavailable for this chat.";
4817
+ return this.t(locale, "common:errors.no_telegram_identity");
4396
4818
  }
4397
4819
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4398
4820
  if (!activeSessionId) {
4399
- return "No active session is linked yet. Pair a session via /start <code>.";
4821
+ return this.t(locale, "common:errors.no_active_session");
4400
4822
  }
4401
4823
  const session = await this.sessionStore.getSession(activeSessionId);
4402
4824
  if (!session?.linkedSessionId) {
4403
4825
  return [
4404
- "🤝 Partner",
4826
+ this.t(locale, "menu:partner.screen.title"),
4405
4827
  "",
4406
- `📌 Active session: ${session?.label ?? activeSessionId}`,
4828
+ this.t(locale, "menu:partner.screen.active_session", {
4829
+ sessionName: session?.label ?? activeSessionId,
4830
+ }),
4407
4831
  "",
4408
- "No partner is linked yet.",
4409
- "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"),
4410
4834
  ].join("\n");
4411
4835
  }
4412
4836
  const linkedSession = await this.sessionStore.getSession(session.linkedSessionId);
4413
4837
  return [
4414
- "🤝 Partner",
4838
+ this.t(locale, "menu:partner.screen.title"),
4415
4839
  "",
4416
- `📌 Active session: ${session.label ?? activeSessionId}`,
4417
- `👥 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
+ }),
4418
4846
  "",
4419
- "Ask for API details or share what changed.",
4420
- "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"),
4421
4849
  ].join("\n");
4422
4850
  }
4423
4851
  async buildLocalMenuText(ctx) {
4852
+ const locale = await this.resolveLocaleForContext(ctx);
4424
4853
  const principal = this.getPrincipalFromContext(ctx);
4425
4854
  if (!principal) {
4426
- return "Локальное взаимодействие недоступно для этого чата.";
4855
+ return this.t(locale, "menu:local.screen.unavailable");
4427
4856
  }
4428
4857
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4429
4858
  if (!activeSessionId) {
4430
- return "Активная сессия не выбрана. Сначала привяжи её через /start.";
4859
+ return this.t(locale, "menu:local.screen.no_active_session");
4431
4860
  }
4432
4861
  const session = await this.sessionStore.getSession(activeSessionId);
4433
4862
  const linkedSession = session?.linkedSessionId
4434
4863
  ? await this.sessionStore.getSession(session.linkedSessionId)
4435
4864
  : null;
4436
4865
  return [
4437
- "🏠 Local",
4866
+ this.t(locale, "menu:main.buttons.local"),
4438
4867
  "",
4439
- `📌 Активная сессия: ${session?.label ?? activeSessionId}`,
4440
- `🤝 Связь: ${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"),
4441
4876
  "",
4442
- "Здесь живёт локальная работа в одном боте:",
4443
- "связка сессий, обмен note и файлами без gateway.",
4877
+ this.t(locale, "menu:local.screen.hint_title"),
4878
+ this.t(locale, "menu:local.screen.hint_body"),
4444
4879
  ].join("\n");
4445
4880
  }
4446
4881
  async buildProjectsMenuText(ctx) {
4882
+ const locale = await this.resolveLocaleForContext(ctx);
4447
4883
  const { session, projects } = await this.loadProjectsContext(ctx);
4448
4884
  if (!this.config.distributed.gatewayPublicUrl) {
4449
4885
  return [
4450
- "👥 Collab",
4886
+ this.t(locale, "menu:collab.screen.title"),
4451
4887
  "",
4452
- "Gateway не настроен для этого запуска.",
4453
- "Для локальной работы в одном боте используй раздел Local.",
4888
+ this.t(locale, "menu:collab.screen.gateway_not_configured"),
4889
+ this.t(locale, "menu:collab.screen.use_local_instead"),
4454
4890
  ].join("\n");
4455
4891
  }
4456
4892
  if (!session || !projects) {
4457
- return "Collab недоступен для текущей сессии.";
4893
+ return this.t(locale, "menu:collab.screen.unavailable");
4458
4894
  }
4459
4895
  return [
4460
- "👥 Collab",
4896
+ this.t(locale, "menu:collab.screen.title"),
4461
4897
  "",
4462
- `📌 Активная сессия: ${session.label ?? session.sessionId}`,
4463
- `📦 Открытый проект: ${session.activeProjectName ?? "не выбран"}`,
4464
- `🗂 Доступно проектов: ${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
+ }),
4465
4909
  "",
4466
- "Открой проект, создай новый или войди по invite-коду.",
4910
+ this.t(locale, "menu:collab.screen.invite_hint"),
4467
4911
  ].join("\n");
4468
4912
  }
4469
4913
  async buildCollabToolsMenuText(ctx) {
4914
+ const locale = await this.resolveLocaleForContext(ctx);
4470
4915
  const principal = this.getPrincipalFromContext(ctx);
4471
4916
  if (!principal || !this.config.distributed.gatewayPublicUrl) {
4472
- return "Collab tools недоступны для этого запуска.";
4917
+ return this.t(locale, "menu:collab.screen.gateway_not_configured");
4473
4918
  }
4474
4919
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4475
4920
  if (!sessionId) {
4476
- return "Активная сессия не выбрана.";
4921
+ return this.t(locale, "common:errors.no_active_session");
4477
4922
  }
4478
4923
  const session = await this.sessionStore.getSession(sessionId);
4479
4924
  const projects = await this.listGatewayProjects(principal);
4480
4925
  if (projects.length === 0) {
4481
4926
  return [
4482
- "🛠 Collab Tools",
4927
+ this.t(locale, "menu:collab.screen.tools_title"),
4483
4928
  "",
4484
- `📌 Активная сессия: ${session?.label ?? sessionId}`,
4929
+ this.t(locale, "menu:collab.screen.active_session", {
4930
+ sessionName: session?.label ?? sessionId,
4931
+ }),
4485
4932
  "",
4486
- "Сначала создай проект или войди в существующий.",
4933
+ this.t(locale, "menu:collab.screen.tools_empty"),
4487
4934
  ].join("\n");
4488
4935
  }
4489
4936
  const targets = await this.collectCollabBroadcastTargets(principal, sessionId);
4490
4937
  const uniqueCount = targets.localTargetSessionIds.length + targets.remoteTargets.length;
4491
4938
  return [
4492
- "🛠 Collab Tools",
4939
+ this.t(locale, "menu:collab.screen.tools_title"),
4493
4940
  "",
4494
- `📌 Активная сессия: ${session?.label ?? sessionId}`,
4495
- `🗂 Проектов в Collab: ${projects.length}`,
4496
- `👥 Уникальных сессий: ${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
+ }),
4497
4950
  "",
4498
- "Broadcast отправит следующее текстовое сообщение всем уникальным Collab-сессиям на ботах без дублирования.",
4499
- "History отправит .md с последними 5 Collab-событиями текущей сессии.",
4500
- "Локальные сессии получат 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"),
4501
4954
  ].join("\n");
4502
4955
  }
4503
4956
  async buildCollabDeleteMenuText(ctx) {
4957
+ const locale = await this.resolveLocaleForContext(ctx);
4504
4958
  const { session, projects } = await this.loadProjectsContext(ctx);
4505
4959
  if (!this.config.distributed.gatewayPublicUrl) {
4506
- return "Удаление проектов недоступно для этого запуска.";
4960
+ return this.t(locale, "menu:collab.screen.gateway_not_configured");
4507
4961
  }
4508
4962
  if (!session || !projects) {
4509
- return "Удаление проектов недоступно для текущей сессии.";
4963
+ return this.t(locale, "menu:collab.screen.unavailable");
4510
4964
  }
4511
4965
  const ownerCount = projects.filter((project) => project.role === "owner").length;
4512
4966
  return [
4513
- "🗑 Delete Project",
4967
+ this.t(locale, "menu:project.delete_menu_title"),
4514
4968
  "",
4515
- `📌 Активная сессия: ${session.label ?? session.sessionId}`,
4516
- `🗂 Проектов в Collab: ${projects.length}`,
4517
- `👑 Проектов, где ты 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
+ }),
4518
4978
  "",
4519
- "Выбери проект для полного удаления.",
4520
- "Будут отвязаны участники, удалены project sessions и сама запись проекта.",
4521
- "Удалять проект может только 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"),
4522
4982
  ].join("\n");
4523
4983
  }
4524
4984
  async buildDeveloperMenuText(ctx) {
4985
+ const locale = await this.resolveLocaleForContext(ctx);
4525
4986
  const principal = this.getPrincipalFromContext(ctx);
4526
4987
  if (!principal) {
4527
- return "Telegram identity is unavailable for this chat.";
4988
+ return this.t(locale, "common:errors.no_telegram_identity");
4528
4989
  }
4529
4990
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4530
4991
  return [
4531
- "🛠 Tools",
4992
+ this.t(locale, "menu:developer.screen.title"),
4532
4993
  "",
4533
- `🔗 Linked sessions: ${sessionIds.length}`,
4994
+ this.t(locale, "menu:developer.screen.linked_sessions", {
4995
+ count: sessionIds.length,
4996
+ }),
4534
4997
  "",
4535
- "Broadcast writes your next text message into every linked session inbox and nudges all configured tmux targets.",
4536
- "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"),
4537
5000
  ].join("\n");
4538
5001
  }
4539
5002
  async buildUnpairConfirmText(ctx) {
5003
+ const locale = await this.resolveLocaleForContext(ctx);
4540
5004
  const principal = this.getPrincipalFromContext(ctx);
4541
5005
  if (!principal) {
4542
- return "Telegram identity is unavailable for this chat.";
5006
+ return this.t(locale, "common:errors.no_telegram_identity");
4543
5007
  }
4544
5008
  const activeSessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4545
5009
  if (!activeSessionId) {
4546
- return "No active session selected.";
5010
+ return this.t(locale, "common:errors.no_active_session");
4547
5011
  }
4548
5012
  const session = await this.sessionStore.getSession(activeSessionId);
4549
5013
  return [
4550
- "⚠ Confirm unpair",
5014
+ this.t(locale, "menu:unpair.title"),
4551
5015
  "",
4552
- `📌 Active session: ${session?.label ?? activeSessionId}`,
5016
+ this.t(locale, "menu:unpair.active_session", {
5017
+ sessionName: session?.label ?? activeSessionId,
5018
+ }),
4553
5019
  "",
4554
- "This removes the Telegram binding for the active session.",
4555
- "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"),
4556
5022
  ].join("\n");
4557
5023
  }
4558
5024
  async buildPruneConfirmText(ctx) {
5025
+ const locale = await this.resolveLocaleForContext(ctx);
4559
5026
  const principal = this.getPrincipalFromContext(ctx);
4560
5027
  if (!principal) {
4561
- return "Telegram identity is unavailable for this chat.";
5028
+ return this.t(locale, "common:errors.no_telegram_identity");
4562
5029
  }
4563
5030
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4564
5031
  return [
4565
- "⚠ Confirm prune",
5032
+ this.t(locale, "menu:prune.title"),
4566
5033
  "",
4567
- `🔗 Linked sessions visible here: ${sessionIds.length}`,
5034
+ this.t(locale, "menu:prune.linked_sessions", {
5035
+ count: sessionIds.length,
5036
+ }),
4568
5037
  "",
4569
- "This clears every Redis key under the telegram-mcp namespace.",
4570
- "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"),
4571
5040
  ].join("\n");
4572
5041
  }
4573
5042
  async showActiveSessionInfo(ctx) {
5043
+ const locale = await this.resolveLocaleForContext(ctx);
4574
5044
  const principal = this.getPrincipalFromContext(ctx);
4575
5045
  if (!principal) {
4576
5046
  await ctx.answerCallbackQuery({
4577
- text: "Telegram identity is unavailable.",
5047
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4578
5048
  show_alert: true,
4579
5049
  });
4580
5050
  return;
@@ -4582,7 +5052,7 @@ class TelegramTransport {
4582
5052
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4583
5053
  if (!sessionId) {
4584
5054
  await ctx.answerCallbackQuery({
4585
- text: "No active session selected.",
5055
+ text: this.t(locale, "common:errors.no_active_session"),
4586
5056
  show_alert: true,
4587
5057
  });
4588
5058
  return;
@@ -4593,23 +5063,55 @@ class TelegramTransport {
4593
5063
  const linkedSession = session?.linkedSessionId
4594
5064
  ? await this.sessionStore.getSession(session.linkedSessionId)
4595
5065
  : null;
4596
- await ctx.answerCallbackQuery({ text: "Session info opened." });
5066
+ await ctx.answerCallbackQuery({
5067
+ text: this.t(locale, "menu:session_info.opened"),
5068
+ });
4597
5069
  await this.replyText(ctx, [
4598
- "ℹ Session info",
5070
+ this.t(locale, "menu:session_info.title"),
4599
5071
  "",
4600
- `📌 Label: ${session?.label ?? sessionId}`,
4601
- `🆔 Session ID: ${sessionId}`,
4602
- `📥 Inbox count: ${inboxCount}`,
4603
- `🔗 Paired: ${binding ? "yes" : "no"}`,
4604
- `🤝 Partner: ${linkedSession?.label ?? session?.linkedSessionId ?? "not linked"}`,
4605
- `🖥 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
+ }),
4606
5094
  ...(session?.tmuxSessionName
4607
- ? [`📺 tmux session: ${session.tmuxSessionName}`]
5095
+ ? [
5096
+ this.t(locale, "menu:session_info.tmux_session", {
5097
+ value: session.tmuxSessionName,
5098
+ }),
5099
+ ]
4608
5100
  : []),
4609
5101
  ...(session?.tmuxWindowName
4610
- ? [`🪟 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
+ ]
4611
5114
  : []),
4612
- ...(session?.tmuxPaneId ? [`🔹 tmux pane: ${session.tmuxPaneId}`] : []),
4613
5115
  ].join("\n"), { kind: "menu", sessionId }, { reply_markup: this.settingsMenu });
4614
5116
  }
4615
5117
  async linkSessions(sessionId, targetSessionId) {
@@ -4750,10 +5252,11 @@ class TelegramTransport {
4750
5252
  }
4751
5253
  }
4752
5254
  async unpairActiveSession(ctx) {
5255
+ const locale = await this.resolveLocaleForContext(ctx);
4753
5256
  const principal = this.getPrincipalFromContext(ctx);
4754
5257
  if (!principal) {
4755
5258
  await ctx.answerCallbackQuery({
4756
- text: "Telegram identity is unavailable.",
5259
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4757
5260
  show_alert: true,
4758
5261
  });
4759
5262
  return;
@@ -4761,7 +5264,7 @@ class TelegramTransport {
4761
5264
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4762
5265
  if (!sessionId) {
4763
5266
  await ctx.answerCallbackQuery({
4764
- text: "No active session selected.",
5267
+ text: this.t(locale, "common:errors.no_active_session"),
4765
5268
  show_alert: true,
4766
5269
  });
4767
5270
  return;
@@ -4790,19 +5293,20 @@ class TelegramTransport {
4790
5293
  });
4791
5294
  this.clearPendingInteractionsForContext(ctx);
4792
5295
  await ctx.answerCallbackQuery({
4793
- text: session?.label
4794
- ? `Unpaired: ${session.label}`
4795
- : `Unpaired: ${sessionId}`,
5296
+ text: this.t(locale, "menu:unpair.done", {
5297
+ sessionName: session?.label ?? sessionId,
5298
+ }),
4796
5299
  });
4797
- await this.showSessionsMenu(ctx, session?.label
4798
- ? `Session unpaired: ${session.label}`
4799
- : `Session unpaired: ${sessionId}`);
5300
+ await this.showSessionsMenu(ctx, this.t(locale, "menu:unpair.shown", {
5301
+ sessionName: session?.label ?? sessionId,
5302
+ }));
4800
5303
  }
4801
5304
  async beginRenameActiveSession(ctx) {
5305
+ const locale = await this.resolveLocaleForContext(ctx);
4802
5306
  const principal = this.getPrincipalFromContext(ctx);
4803
5307
  if (!principal) {
4804
5308
  await ctx.answerCallbackQuery({
4805
- text: "Telegram identity is unavailable.",
5309
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4806
5310
  show_alert: true,
4807
5311
  });
4808
5312
  return;
@@ -4810,7 +5314,7 @@ class TelegramTransport {
4810
5314
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4811
5315
  if (!sessionId) {
4812
5316
  await ctx.answerCallbackQuery({
4813
- text: "No active session selected.",
5317
+ text: this.t(locale, "common:errors.no_active_session"),
4814
5318
  show_alert: true,
4815
5319
  });
4816
5320
  return;
@@ -4818,19 +5322,17 @@ class TelegramTransport {
4818
5322
  const principalKey = buildPrincipalKey(principal);
4819
5323
  this.pendingBroadcasts.delete(principalKey);
4820
5324
  this.pendingRenames.set(principalKey, { sessionId });
4821
- await ctx.answerCallbackQuery({ text: "Send the new session title." });
4822
- await this.replyText(ctx, [
4823
- "✏ Rename session",
4824
- "",
4825
- "Send the next text message as the new title for the active session.",
4826
- "Commands like /menu or /help will cancel rename mode.",
4827
- ].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 });
4828
5329
  }
4829
5330
  async beginBroadcast(ctx) {
5331
+ const locale = await this.resolveLocaleForContext(ctx);
4830
5332
  const principal = this.getPrincipalFromContext(ctx);
4831
5333
  if (!principal) {
4832
5334
  await ctx.answerCallbackQuery({
4833
- text: "Telegram identity is unavailable.",
5335
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4834
5336
  show_alert: true,
4835
5337
  });
4836
5338
  return;
@@ -4838,7 +5340,7 @@ class TelegramTransport {
4838
5340
  const sessionIds = await this.bindingStore.listBoundSessionIdsForPrincipal(principal);
4839
5341
  if (sessionIds.length === 0) {
4840
5342
  await ctx.answerCallbackQuery({
4841
- text: "No linked sessions found.",
5343
+ text: this.t(locale, "menu:broadcast.no_linked_sessions"),
4842
5344
  show_alert: true,
4843
5345
  });
4844
5346
  return;
@@ -4846,14 +5348,18 @@ class TelegramTransport {
4846
5348
  const principalKey = buildPrincipalKey(principal);
4847
5349
  this.pendingRenames.delete(principalKey);
4848
5350
  await ctx.answerCallbackQuery({
4849
- text: `Broadcast to ${sessionIds.length} sessions.`,
5351
+ text: this.t(locale, "menu:broadcast.begin", {
5352
+ count: sessionIds.length,
5353
+ }),
4850
5354
  });
4851
5355
  const sent = await this.replyText(ctx, [
4852
- "📣 Broadcast",
5356
+ this.t(locale, "menu:broadcast.title"),
4853
5357
  "",
4854
- `Send the next text message to broadcast it to all ${sessionIds.length} linked sessions.`,
4855
- "The message will be stored in every session inbox and the service will nudge every configured tmux target.",
4856
- "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"),
4857
5363
  ].join("\n"), { kind: "menu" }, {
4858
5364
  reply_markup: new grammy_1.InlineKeyboard().text("Cancel", "broadcast-cancel"),
4859
5365
  });
@@ -4867,10 +5373,11 @@ class TelegramTransport {
4867
5373
  });
4868
5374
  }
4869
5375
  async beginProjectBroadcast(ctx) {
5376
+ const locale = await this.resolveLocaleForContext(ctx);
4870
5377
  const principal = this.getPrincipalFromContext(ctx);
4871
5378
  if (!principal) {
4872
5379
  await ctx.answerCallbackQuery({
4873
- text: "Telegram identity is unavailable.",
5380
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4874
5381
  show_alert: true,
4875
5382
  });
4876
5383
  return;
@@ -4878,7 +5385,7 @@ class TelegramTransport {
4878
5385
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
4879
5386
  if (!sessionId) {
4880
5387
  await ctx.answerCallbackQuery({
4881
- text: "Активная сессия не выбрана.",
5388
+ text: this.t(locale, "common:errors.no_active_session"),
4882
5389
  show_alert: true,
4883
5390
  });
4884
5391
  return;
@@ -4886,7 +5393,7 @@ class TelegramTransport {
4886
5393
  const projects = await this.listGatewayProjects(principal);
4887
5394
  if (projects.length === 0) {
4888
5395
  await ctx.answerCallbackQuery({
4889
- text: "Сначала создай проект или войди в существующий.",
5396
+ text: this.t(locale, "menu:broadcast.no_projects_first"),
4890
5397
  show_alert: true,
4891
5398
  });
4892
5399
  return;
@@ -4895,7 +5402,7 @@ class TelegramTransport {
4895
5402
  const totalTargets = targets.localTargetSessionIds.length + targets.remoteTargets.length;
4896
5403
  if (totalTargets === 0) {
4897
5404
  await ctx.answerCallbackQuery({
4898
- text: "Нет доступных Collab-сессий для broadcast.",
5405
+ text: this.t(locale, "menu:broadcast.no_collab_targets"),
4899
5406
  show_alert: true,
4900
5407
  });
4901
5408
  return;
@@ -4903,17 +5410,23 @@ class TelegramTransport {
4903
5410
  const principalKey = buildPrincipalKey(principal);
4904
5411
  this.pendingRenames.delete(principalKey);
4905
5412
  await ctx.answerCallbackQuery({
4906
- text: `Broadcast to ${totalTargets} collab sessions.`,
5413
+ text: this.t(locale, "menu:broadcast.collab_begin", {
5414
+ count: totalTargets,
5415
+ }),
4907
5416
  });
4908
5417
  const sent = await this.replyText(ctx, [
4909
- "📣 Collab Broadcast",
5418
+ this.t(locale, "menu:broadcast.collab_title"),
4910
5419
  "",
4911
- `Collab проектов: ${projects.length}`,
4912
- `Уникальных сессий: ${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
+ }),
4913
5426
  "",
4914
- "Отправь следующее текстовое сообщение, чтобы разослать его всем Collab-сессиям на ботах без дублирования.",
4915
- "Локальные сессии получат inbox message, удалённые — gateway delivery.",
4916
- "Команды вроде /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"),
4917
5430
  ].join("\n"), { kind: "menu", sessionId }, {
4918
5431
  reply_markup: new grammy_1.InlineKeyboard().text("Cancel", "broadcast-cancel"),
4919
5432
  });
@@ -4992,10 +5505,11 @@ class TelegramTransport {
4992
5505
  }
4993
5506
  }
4994
5507
  async cancelPendingBroadcast(ctx) {
5508
+ const locale = await this.resolveLocaleForContext(ctx);
4995
5509
  const principal = this.getPrincipalFromContext(ctx);
4996
5510
  if (!principal) {
4997
5511
  await ctx.answerCallbackQuery({
4998
- text: "Telegram identity is unavailable.",
5512
+ text: this.t(locale, "common:errors.no_telegram_identity"),
4999
5513
  show_alert: true,
5000
5514
  });
5001
5515
  return;
@@ -5004,7 +5518,7 @@ class TelegramTransport {
5004
5518
  const pending = this.pendingBroadcasts.get(principalKey);
5005
5519
  if (!pending) {
5006
5520
  await ctx.answerCallbackQuery({
5007
- text: "Broadcast mode is not active.",
5521
+ text: this.t(locale, "menu:broadcast.mode_not_active"),
5008
5522
  show_alert: true,
5009
5523
  });
5010
5524
  return;
@@ -5013,7 +5527,9 @@ class TelegramTransport {
5013
5527
  await this.deletePendingBroadcastArtifacts(ctx, pending, {
5014
5528
  deleteMenuMessage: false,
5015
5529
  });
5016
- await ctx.answerCallbackQuery({ text: "Broadcast cancelled." });
5530
+ await ctx.answerCallbackQuery({
5531
+ text: this.t(locale, "menu:broadcast.cancelled"),
5532
+ });
5017
5533
  if (pending.scope === "project") {
5018
5534
  await this.showCollabToolsMenu(ctx);
5019
5535
  return;
@@ -5223,12 +5739,22 @@ class TelegramTransport {
5223
5739
  });
5224
5740
  await this.replyText(ctx, pending.scope === "project"
5225
5741
  ? [
5226
- "Collab broadcast completed.",
5227
- `Локальных inbox: ${storedCount}`,
5228
- `Удалённых deliveries: ${remoteCount}`,
5229
- `Всего сессий: ${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
+ }),
5230
5754
  ].join("\n")
5231
- : `Broadcast completed for ${storedCount} linked sessions.`, {
5755
+ : await this.tForContext(ctx, "menu:broadcast.completed_linked", {
5756
+ count: storedCount,
5757
+ }), {
5232
5758
  kind: "menu",
5233
5759
  ...(pending.sessionId ? { sessionId: pending.sessionId } : {}),
5234
5760
  });
@@ -5245,10 +5771,11 @@ class TelegramTransport {
5245
5771
  };
5246
5772
  }
5247
5773
  async beginPartnerNoteMode(ctx, kind, target) {
5774
+ const locale = await this.resolveLocaleForContext(ctx);
5248
5775
  const principal = this.getPrincipalFromContext(ctx);
5249
5776
  if (!principal) {
5250
5777
  await ctx.answerCallbackQuery({
5251
- text: "Telegram identity is unavailable.",
5778
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5252
5779
  show_alert: true,
5253
5780
  });
5254
5781
  return;
@@ -5256,7 +5783,7 @@ class TelegramTransport {
5256
5783
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5257
5784
  if (!sessionId) {
5258
5785
  await ctx.answerCallbackQuery({
5259
- text: "No active session selected.",
5786
+ text: this.t(locale, "common:errors.no_active_session"),
5260
5787
  show_alert: true,
5261
5788
  });
5262
5789
  return;
@@ -5264,7 +5791,7 @@ class TelegramTransport {
5264
5791
  const session = await this.sessionStore.getSession(sessionId);
5265
5792
  if (!target && !session?.linkedSessionId) {
5266
5793
  await ctx.answerCallbackQuery({
5267
- text: "Link a partner session first.",
5794
+ text: this.t(locale, "menu:partner.screen.use_link_first"),
5268
5795
  show_alert: true,
5269
5796
  });
5270
5797
  return;
@@ -5275,7 +5802,7 @@ class TelegramTransport {
5275
5802
  const targetLabel = target?.targetSessionLabel ??
5276
5803
  linkedSession?.label ??
5277
5804
  session?.linkedSessionId ??
5278
- "напарник";
5805
+ this.t(locale, "menu:partner.screen.default_partner");
5279
5806
  const sourceLabel = session?.label ?? sessionId;
5280
5807
  const isProjectTarget = Boolean(target?.projectUuid);
5281
5808
  const prompt = (0, collabUi_1.buildPartnerNotePromptText)({
@@ -5287,7 +5814,7 @@ class TelegramTransport {
5287
5814
  const kindLabel = prompt.kindLabel;
5288
5815
  await ctx.answerCallbackQuery({ text: `${kindLabel}.` });
5289
5816
  const sent = await this.replyText(ctx, prompt.text, { kind: "menu", sessionId }, {
5290
- 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"),
5291
5818
  });
5292
5819
  this.pendingPartnerNotes.set(buildPrincipalKey(principal), {
5293
5820
  sessionId,
@@ -5315,10 +5842,11 @@ class TelegramTransport {
5315
5842
  }
5316
5843
  }
5317
5844
  async cancelPendingPartnerNote(ctx) {
5845
+ const locale = await this.resolveLocaleForContext(ctx);
5318
5846
  const principal = this.getPrincipalFromContext(ctx);
5319
5847
  if (!principal) {
5320
5848
  await ctx.answerCallbackQuery({
5321
- text: "Telegram identity недоступна.",
5849
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5322
5850
  show_alert: true,
5323
5851
  });
5324
5852
  return;
@@ -5327,21 +5855,24 @@ class TelegramTransport {
5327
5855
  const pending = this.pendingPartnerNotes.get(principalKey);
5328
5856
  if (!pending) {
5329
5857
  await ctx.answerCallbackQuery({
5330
- text: "Нет активного ввода для note напарнику.",
5858
+ text: this.t(locale, "menu:partner.actions.no_pending_note_input"),
5331
5859
  show_alert: true,
5332
5860
  });
5333
5861
  return;
5334
5862
  }
5335
5863
  this.pendingPartnerNotes.delete(principalKey);
5336
5864
  await this.deletePendingPartnerNotePrompt(ctx, pending);
5337
- await ctx.answerCallbackQuery({ text: "Отправка note напарнику отменена." });
5865
+ await ctx.answerCallbackQuery({
5866
+ text: this.t(locale, "menu:partner.actions.cancel_note_input"),
5867
+ });
5338
5868
  await this.showPartnerMenu(ctx);
5339
5869
  }
5340
5870
  async beginProjectMode(ctx, mode) {
5871
+ const locale = await this.resolveLocaleForContext(ctx);
5341
5872
  const principal = this.getPrincipalFromContext(ctx);
5342
5873
  if (!principal) {
5343
5874
  await ctx.answerCallbackQuery({
5344
- text: "Telegram identity is unavailable.",
5875
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5345
5876
  show_alert: true,
5346
5877
  });
5347
5878
  return;
@@ -5349,23 +5880,23 @@ class TelegramTransport {
5349
5880
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5350
5881
  if (!sessionId) {
5351
5882
  await ctx.answerCallbackQuery({
5352
- text: "No active session selected.",
5883
+ text: this.t(locale, "common:errors.no_active_session"),
5353
5884
  show_alert: true,
5354
5885
  });
5355
5886
  return;
5356
5887
  }
5357
5888
  const sent = await this.replyText(ctx, mode === "create"
5358
5889
  ? [
5359
- "📦 Создать проект",
5890
+ this.t(locale, "menu:project.create_prompt_title"),
5360
5891
  "",
5361
- "Отправь следующим сообщением имя проекта.",
5362
- "Команды вроде /menu или /help отменят этот режим.",
5892
+ this.t(locale, "menu:project.create_prompt_body"),
5893
+ this.t(locale, "menu:project.prompt_cancel"),
5363
5894
  ].join("\n")
5364
5895
  : [
5365
- "🔑 Вступить в проект",
5896
+ this.t(locale, "menu:project.join_prompt_title"),
5366
5897
  "",
5367
- "Отправь следующим сообщением invite token проекта.",
5368
- "Команды вроде /menu или /help отменят этот режим.",
5898
+ this.t(locale, "menu:project.join_prompt_body"),
5899
+ this.t(locale, "menu:project.prompt_cancel"),
5369
5900
  ].join("\n"), { kind: "menu", sessionId });
5370
5901
  this.pendingProjects.set(buildPrincipalKey(principal), {
5371
5902
  sessionId,
@@ -5374,14 +5905,17 @@ class TelegramTransport {
5374
5905
  ...(sent ? { promptMessageId: sent.message_id } : {}),
5375
5906
  });
5376
5907
  await ctx.answerCallbackQuery({
5377
- text: mode === "create" ? "Создание проекта." : "Вход в проект.",
5908
+ text: mode === "create"
5909
+ ? this.t(locale, "menu:project.start_create")
5910
+ : this.t(locale, "menu:project.start_join"),
5378
5911
  });
5379
5912
  }
5380
5913
  async handleProjectSelect(ctx) {
5914
+ const locale = await this.resolveLocaleForContext(ctx);
5381
5915
  const payloadKey = readMenuPayloadKey(ctx);
5382
5916
  if (!payloadKey) {
5383
5917
  await ctx.answerCallbackQuery({
5384
- text: "Данные проекта не найдены.",
5918
+ text: this.t(locale, "menu:project.data_missing"),
5385
5919
  show_alert: true,
5386
5920
  });
5387
5921
  return;
@@ -5392,7 +5926,7 @@ class TelegramTransport {
5392
5926
  !payload.sessionId ||
5393
5927
  !payload.projectUuid) {
5394
5928
  await ctx.answerCallbackQuery({
5395
- text: "Данные проекта устарели или некорректны.",
5929
+ text: this.t(locale, "menu:project.data_stale"),
5396
5930
  show_alert: true,
5397
5931
  });
5398
5932
  return;
@@ -5400,7 +5934,7 @@ class TelegramTransport {
5400
5934
  const principal = this.getPrincipalFromContext(ctx);
5401
5935
  if (!principal) {
5402
5936
  await ctx.answerCallbackQuery({
5403
- text: "Telegram identity is unavailable.",
5937
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5404
5938
  show_alert: true,
5405
5939
  });
5406
5940
  return;
@@ -5408,7 +5942,7 @@ class TelegramTransport {
5408
5942
  const project = await this.getProjectPayloadByUuid(payload.sessionId, payload.projectUuid);
5409
5943
  if (!project) {
5410
5944
  await ctx.answerCallbackQuery({
5411
- text: "Проект не найден.",
5945
+ text: this.t(locale, "menu:project.not_found"),
5412
5946
  show_alert: true,
5413
5947
  });
5414
5948
  return;
@@ -5419,14 +5953,17 @@ class TelegramTransport {
5419
5953
  projectUuid: project.projectUuid,
5420
5954
  projectName: project.projectName,
5421
5955
  });
5422
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
5956
+ await ctx.answerCallbackQuery({
5957
+ text: this.t(locale, "menu:project.opening_members"),
5958
+ });
5423
5959
  await this.showProjectMembers(ctx, project);
5424
5960
  }
5425
5961
  async handleProjectDeleteSelect(ctx) {
5962
+ const locale = await this.resolveLocaleForContext(ctx);
5426
5963
  const payloadKey = readMenuPayloadKey(ctx);
5427
5964
  if (!payloadKey) {
5428
5965
  await ctx.answerCallbackQuery({
5429
- text: "Данные проекта не найдены.",
5966
+ text: this.t(locale, "menu:project.data_missing"),
5430
5967
  show_alert: true,
5431
5968
  });
5432
5969
  return;
@@ -5437,7 +5974,7 @@ class TelegramTransport {
5437
5974
  !payload.sessionId ||
5438
5975
  !payload.projectUuid) {
5439
5976
  await ctx.answerCallbackQuery({
5440
- text: "Данные проекта устарели или некорректны.",
5977
+ text: this.t(locale, "menu:project.data_stale"),
5441
5978
  show_alert: true,
5442
5979
  });
5443
5980
  await ctx.deleteMessage().catch(() => undefined);
@@ -5446,7 +5983,7 @@ class TelegramTransport {
5446
5983
  const principal = this.getPrincipalFromContext(ctx);
5447
5984
  if (!principal) {
5448
5985
  await ctx.answerCallbackQuery({
5449
- text: "Telegram identity is unavailable.",
5986
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5450
5987
  show_alert: true,
5451
5988
  });
5452
5989
  return;
@@ -5455,7 +5992,7 @@ class TelegramTransport {
5455
5992
  const project = projects.find((item) => item.project_uuid === payload.projectUuid);
5456
5993
  if (!project) {
5457
5994
  await ctx.answerCallbackQuery({
5458
- text: "Проект не найден.",
5995
+ text: this.t(locale, "menu:project.not_found"),
5459
5996
  show_alert: true,
5460
5997
  });
5461
5998
  await ctx.deleteMessage().catch(() => undefined);
@@ -5463,7 +6000,7 @@ class TelegramTransport {
5463
6000
  }
5464
6001
  if (project.role !== "owner") {
5465
6002
  await ctx.answerCallbackQuery({
5466
- text: "Удалять проект может только owner.",
6003
+ text: this.t(locale, "menu:project.delete_only_owner"),
5467
6004
  show_alert: true,
5468
6005
  });
5469
6006
  return;
@@ -5471,10 +6008,11 @@ class TelegramTransport {
5471
6008
  await this.handleProjectDeleteByUuid(ctx, payload.projectUuid);
5472
6009
  }
5473
6010
  async leaveActiveProject(ctx) {
6011
+ const locale = await this.resolveLocaleForContext(ctx);
5474
6012
  const principal = this.getPrincipalFromContext(ctx);
5475
6013
  if (!principal) {
5476
6014
  await ctx.answerCallbackQuery({
5477
- text: "Telegram identity is unavailable.",
6015
+ text: this.t(locale, "common:errors.no_telegram_identity"),
5478
6016
  show_alert: true,
5479
6017
  });
5480
6018
  return;
@@ -5482,7 +6020,7 @@ class TelegramTransport {
5482
6020
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5483
6021
  if (!sessionId) {
5484
6022
  await ctx.answerCallbackQuery({
5485
- text: "No active session selected.",
6023
+ text: this.t(locale, "common:errors.no_active_session"),
5486
6024
  show_alert: true,
5487
6025
  });
5488
6026
  return;
@@ -5490,7 +6028,7 @@ class TelegramTransport {
5490
6028
  const session = await this.sessionStore.getSession(sessionId);
5491
6029
  if (!session?.activeProjectUuid) {
5492
6030
  await ctx.answerCallbackQuery({
5493
- text: "No active project.",
6031
+ text: this.t(locale, "menu:project.no_active_project"),
5494
6032
  show_alert: true,
5495
6033
  });
5496
6034
  return;
@@ -5506,8 +6044,10 @@ class TelegramTransport {
5506
6044
  activeProjectName: undefined,
5507
6045
  updatedAt: new Date().toISOString(),
5508
6046
  });
5509
- await ctx.answerCallbackQuery({ text: "Project left." });
5510
- 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"));
5511
6051
  }
5512
6052
  async showProjectDetail(ctx, input) {
5513
6053
  await this.showProjectMembers(ctx, input);
@@ -5555,6 +6095,7 @@ class TelegramTransport {
5555
6095
  if (!binding) {
5556
6096
  throw new Error("Binding is missing for project members screen.");
5557
6097
  }
6098
+ const locale = await this.resolveLocaleForTelegramUserId(binding.telegramUserId);
5558
6099
  const sessions = await this.listGatewayProjectSessions({
5559
6100
  telegramChatId: binding.telegramChatId,
5560
6101
  telegramUserId: binding.telegramUserId,
@@ -5562,19 +6103,25 @@ class TelegramTransport {
5562
6103
  const activeSessionId = session?.sessionId ?? null;
5563
6104
  const selectableMembers = sessions.filter((item) => item.local_session_id !== activeSessionId);
5564
6105
  const lines = [
5565
- `👥 Участники ${escapeHtml(input.projectName)}`,
6106
+ this.t(locale, "menu:project.members_title", {
6107
+ projectName: escapeHtml(input.projectName),
6108
+ }),
5566
6109
  "",
5567
6110
  `UUID: ${input.projectUuid}`,
5568
6111
  `Invite: <code>${escapeHtml(input.inviteToken)}</code>`,
5569
6112
  "",
5570
- `Сессия: ${escapeHtml(session?.label ?? input.sessionId)}`,
5571
- `Других сессий: ${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
+ }),
5572
6119
  "",
5573
6120
  selectableMembers.length > 0
5574
6121
  ? options?.filePath
5575
- ? "Выбери, кому передать этот файл."
5576
- : "Выбери сессию, чтобы спросить, поделиться, ответить или передать."
5577
- : этом проекте пока нет других активных сессий.",
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"),
5578
6125
  ];
5579
6126
  const keyboard = new grammy_1.InlineKeyboard();
5580
6127
  for (const member of selectableMembers) {
@@ -5598,11 +6145,12 @@ class TelegramTransport {
5598
6145
  keyboard.text(buttonLabel, `project-member-open:${payloadKey}`).row();
5599
6146
  }
5600
6147
  keyboard
5601
- .text("🚪 Выйти", `project-leave:${input.projectUuid}`)
5602
- .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");
5603
6150
  return { text: lines.join("\n"), keyboard };
5604
6151
  }
5605
6152
  async showProjectMemberDetail(ctx, input) {
6153
+ const locale = await this.resolveLocaleForContext(ctx);
5606
6154
  const principal = this.getPrincipalFromContext(ctx);
5607
6155
  if (principal) {
5608
6156
  await this.ensureOpenedProjectIsActive({
@@ -5637,8 +6185,8 @@ class TelegramTransport {
5637
6185
  : {}),
5638
6186
  });
5639
6187
  const keyboard = new grammy_1.InlineKeyboard()
5640
- .text("❓ Спросить", `project-member-note:question:${payloadKey}`)
5641
- .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}`)
5642
6190
  .row();
5643
6191
  if (this.config.webapp.enabled &&
5644
6192
  this.config.distributed.gatewayPublicUrl &&
@@ -5647,7 +6195,7 @@ class TelegramTransport {
5647
6195
  input.targetLocalSessionId) {
5648
6196
  keyboard.text("🖥 Live", `project-member-live:${payloadKey}`).row();
5649
6197
  }
5650
- keyboard.text("⬅ К участникам", `project-members:${input.projectUuid}`);
6198
+ keyboard.text(this.t(locale, "menu:project.back_to_members"), `project-members:${input.projectUuid}`);
5651
6199
  if (ctx.callbackQuery?.message) {
5652
6200
  if (principal && ctx.chat && "message_id" in ctx.callbackQuery.message) {
5653
6201
  this.webAppLaunchRegistry.set(principal.telegramUserId, input.sessionId, this.config.webapp.initDataTtlSeconds, {
@@ -5669,6 +6217,7 @@ class TelegramTransport {
5669
6217
  }
5670
6218
  }
5671
6219
  async showProjectMemberFiles(ctx, input) {
6220
+ const locale = await this.resolveLocaleForContext(ctx);
5672
6221
  const principal = this.getPrincipalFromContext(ctx);
5673
6222
  if (principal) {
5674
6223
  await this.ensureOpenedProjectIsActive({
@@ -5680,14 +6229,18 @@ class TelegramTransport {
5680
6229
  }
5681
6230
  const files = await this.listActiveSessionFiles(input.sessionId);
5682
6231
  const lines = [
5683
- "📎 Выбор файла",
6232
+ this.t(locale, "menu:project.file_title"),
5684
6233
  "",
5685
- `Проект: ${input.projectName}`,
5686
- `Получатель: ${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
+ }),
5687
6240
  "",
5688
6241
  files.length > 0
5689
- ? "Выбери файл для отправки."
5690
- : этой сессии нет загруженных файлов.",
6242
+ ? this.t(locale, "menu:project.file_choose")
6243
+ : this.t(locale, "menu:project.file_none"),
5691
6244
  ];
5692
6245
  const keyboard = new grammy_1.InlineKeyboard();
5693
6246
  for (const filePath of files) {
@@ -5704,7 +6257,7 @@ class TelegramTransport {
5704
6257
  });
5705
6258
  keyboard.text(label, `project-member-open:${payloadKey}`).row();
5706
6259
  }
5707
- 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, {
5708
6261
  ...(input.targetClientUuid
5709
6262
  ? { targetClientUuid: input.targetClientUuid }
5710
6263
  : {}),
@@ -5826,11 +6379,12 @@ class TelegramTransport {
5826
6379
  return data.slice(prefix.length) || null;
5827
6380
  }
5828
6381
  async handleProjectSetCallback(ctx) {
6382
+ const locale = await this.resolveLocaleForContext(ctx);
5829
6383
  const projectUuid = this.extractCallbackSuffix(ctx, "project-set:");
5830
6384
  const principal = this.getPrincipalFromContext(ctx);
5831
6385
  if (!projectUuid || !principal) {
5832
6386
  await ctx.answerCallbackQuery({
5833
- text: "Некорректное действие проекта.",
6387
+ text: this.t(locale, "menu:project.invalid_action"),
5834
6388
  show_alert: true,
5835
6389
  });
5836
6390
  return;
@@ -5838,7 +6392,7 @@ class TelegramTransport {
5838
6392
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5839
6393
  if (!sessionId) {
5840
6394
  await ctx.answerCallbackQuery({
5841
- text: "Активная сессия не выбрана.",
6395
+ text: this.t(locale, "common:errors.no_active_session"),
5842
6396
  show_alert: true,
5843
6397
  });
5844
6398
  return;
@@ -5846,7 +6400,7 @@ class TelegramTransport {
5846
6400
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
5847
6401
  if (!payload) {
5848
6402
  await ctx.answerCallbackQuery({
5849
- text: "Проект не найден.",
6403
+ text: this.t(locale, "menu:project.not_found"),
5850
6404
  show_alert: true,
5851
6405
  });
5852
6406
  return;
@@ -5857,15 +6411,18 @@ class TelegramTransport {
5857
6411
  projectUuid: payload.projectUuid,
5858
6412
  projectName: payload.projectName,
5859
6413
  });
5860
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
6414
+ await ctx.answerCallbackQuery({
6415
+ text: this.t(locale, "menu:project.opening_members"),
6416
+ });
5861
6417
  await this.showProjectMembers(ctx, payload);
5862
6418
  }
5863
6419
  async handleProjectDetailCallback(ctx) {
6420
+ const locale = await this.resolveLocaleForContext(ctx);
5864
6421
  const projectUuid = this.extractCallbackSuffix(ctx, "project-detail:");
5865
6422
  const principal = this.getPrincipalFromContext(ctx);
5866
6423
  if (!projectUuid || !principal) {
5867
6424
  await ctx.answerCallbackQuery({
5868
- text: "Некорректное действие проекта.",
6425
+ text: this.t(locale, "menu:project.invalid_action"),
5869
6426
  show_alert: true,
5870
6427
  });
5871
6428
  return;
@@ -5873,7 +6430,7 @@ class TelegramTransport {
5873
6430
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
5874
6431
  if (!sessionId) {
5875
6432
  await ctx.answerCallbackQuery({
5876
- text: "Активная сессия не выбрана.",
6433
+ text: this.t(locale, "common:errors.no_active_session"),
5877
6434
  show_alert: true,
5878
6435
  });
5879
6436
  return;
@@ -5881,7 +6438,7 @@ class TelegramTransport {
5881
6438
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
5882
6439
  if (!payload) {
5883
6440
  await ctx.answerCallbackQuery({
5884
- text: "Проект не найден.",
6441
+ text: this.t(locale, "menu:project.not_found"),
5885
6442
  show_alert: true,
5886
6443
  });
5887
6444
  return;
@@ -5892,14 +6449,17 @@ class TelegramTransport {
5892
6449
  projectUuid: payload.projectUuid,
5893
6450
  projectName: payload.projectName,
5894
6451
  });
5895
- await ctx.answerCallbackQuery({ text: "Открываю участников проекта." });
6452
+ await ctx.answerCallbackQuery({
6453
+ text: this.t(locale, "menu:project.opening_members"),
6454
+ });
5896
6455
  await this.showProjectMembers(ctx, payload);
5897
6456
  }
5898
6457
  async handleProjectDeleteCallback(ctx) {
6458
+ const locale = await this.resolveLocaleForContext(ctx);
5899
6459
  const projectUuid = this.extractCallbackSuffix(ctx, "project-delete:");
5900
6460
  if (!projectUuid) {
5901
6461
  await ctx.answerCallbackQuery({
5902
- text: "Некорректное действие проекта.",
6462
+ text: this.t(locale, "menu:project.invalid_action"),
5903
6463
  show_alert: true,
5904
6464
  });
5905
6465
  return;
@@ -5907,10 +6467,11 @@ class TelegramTransport {
5907
6467
  await this.handleProjectDeleteByUuid(ctx, projectUuid);
5908
6468
  }
5909
6469
  async handleProjectMemberOpenCallback(ctx) {
6470
+ const locale = await this.resolveLocaleForContext(ctx);
5910
6471
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-open:");
5911
6472
  if (!payloadKey) {
5912
6473
  await ctx.answerCallbackQuery({
5913
- text: "Некорректные данные участника проекта.",
6474
+ text: this.t(locale, "menu:project.invalid_member_payload"),
5914
6475
  show_alert: true,
5915
6476
  });
5916
6477
  return;
@@ -5918,7 +6479,7 @@ class TelegramTransport {
5918
6479
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
5919
6480
  if (!payload) {
5920
6481
  await ctx.answerCallbackQuery({
5921
- text: "Данные участника проекта некорректны или устарели.",
6482
+ text: this.t(locale, "menu:project.stale_member_payload"),
5922
6483
  show_alert: true,
5923
6484
  });
5924
6485
  await ctx.deleteMessage().catch(() => undefined);
@@ -5934,15 +6495,18 @@ class TelegramTransport {
5934
6495
  });
5935
6496
  return;
5936
6497
  }
5937
- await ctx.answerCallbackQuery({ text: "Открываю сессию." });
6498
+ await ctx.answerCallbackQuery({
6499
+ text: this.t(locale, "menu:project.opening_session"),
6500
+ });
5938
6501
  await this.showProjectMemberDetail(ctx, payload);
5939
6502
  }
5940
6503
  async handleProjectMemberNoteCallback(ctx) {
6504
+ const locale = await this.resolveLocaleForContext(ctx);
5941
6505
  const data = ctx.callbackQuery?.data ?? "";
5942
6506
  const match = data.match(/^project-member-note:(question|share):(.+)$/u);
5943
6507
  if (!match) {
5944
6508
  await ctx.answerCallbackQuery({
5945
- text: "Некорректное действие для участника проекта.",
6509
+ text: this.t(locale, "menu:project.invalid_member_action"),
5946
6510
  show_alert: true,
5947
6511
  });
5948
6512
  return;
@@ -5951,7 +6515,7 @@ class TelegramTransport {
5951
6515
  const payloadKey = payloadKeyRaw?.trim();
5952
6516
  if (!payloadKey) {
5953
6517
  await ctx.answerCallbackQuery({
5954
- text: "Некорректные данные участника проекта.",
6518
+ text: this.t(locale, "menu:project.invalid_member_payload"),
5955
6519
  show_alert: true,
5956
6520
  });
5957
6521
  return;
@@ -5959,7 +6523,7 @@ class TelegramTransport {
5959
6523
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
5960
6524
  if (!payload) {
5961
6525
  await ctx.answerCallbackQuery({
5962
- text: "Данные участника проекта некорректны или устарели.",
6526
+ text: this.t(locale, "menu:project.stale_member_payload"),
5963
6527
  show_alert: true,
5964
6528
  });
5965
6529
  await ctx.deleteMessage().catch(() => undefined);
@@ -5972,10 +6536,11 @@ class TelegramTransport {
5972
6536
  });
5973
6537
  }
5974
6538
  async handleProjectMemberLiveCallback(ctx) {
6539
+ const locale = await this.resolveLocaleForContext(ctx);
5975
6540
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-live:");
5976
6541
  if (!payloadKey) {
5977
6542
  await ctx.answerCallbackQuery({
5978
- text: "Некорректные данные Live View.",
6543
+ text: this.t(locale, "menu:project.invalid_live_payload"),
5979
6544
  show_alert: true,
5980
6545
  });
5981
6546
  return;
@@ -5983,7 +6548,7 @@ class TelegramTransport {
5983
6548
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
5984
6549
  if (!payload || !payload.targetClientUuid || !payload.targetLocalSessionId) {
5985
6550
  await ctx.answerCallbackQuery({
5986
- text: "Данные Live View некорректны или устарели.",
6551
+ text: this.t(locale, "menu:project.stale_live_payload"),
5987
6552
  show_alert: true,
5988
6553
  });
5989
6554
  await ctx.deleteMessage().catch(() => undefined);
@@ -5992,7 +6557,7 @@ class TelegramTransport {
5992
6557
  const principal = this.getPrincipalFromContext(ctx);
5993
6558
  if (!principal) {
5994
6559
  await ctx.answerCallbackQuery({
5995
- text: "Не удалось определить Telegram пользователя.",
6560
+ text: this.t(locale, "menu:project.no_telegram_user"),
5996
6561
  show_alert: true,
5997
6562
  });
5998
6563
  return;
@@ -6017,17 +6582,18 @@ class TelegramTransport {
6017
6582
  });
6018
6583
  await ctx.answerCallbackQuery({
6019
6584
  text: result?.delivered
6020
- ? "Запрос на Live отправлен на подтверждение."
6021
- : "Сессия сейчас недоступна для подтверждения.",
6585
+ ? this.t(locale, "menu:project.request_live_sent")
6586
+ : this.t(locale, "menu:live.actions.approval_unavailable"),
6022
6587
  ...(result?.delivered ? {} : { show_alert: true }),
6023
6588
  });
6024
6589
  }
6025
6590
  async handleLiveApprovalCallback(ctx) {
6591
+ const locale = await this.resolveLocaleForContext(ctx);
6026
6592
  const data = ctx.callbackQuery?.data ?? "";
6027
6593
  const match = data.match(/^live-approval:(approve|deny):(.+)$/u);
6028
6594
  if (!match) {
6029
6595
  await ctx.answerCallbackQuery({
6030
- text: "Некорректное подтверждение Live View.",
6596
+ text: this.t(locale, "menu:project.invalid_approval"),
6031
6597
  show_alert: true,
6032
6598
  });
6033
6599
  return;
@@ -6036,7 +6602,7 @@ class TelegramTransport {
6036
6602
  const payloadKey = payloadKeyRaw?.trim();
6037
6603
  if (!payloadKey) {
6038
6604
  await ctx.answerCallbackQuery({
6039
- text: "Некорректные данные подтверждения.",
6605
+ text: this.t(locale, "menu:project.invalid_approval_data"),
6040
6606
  show_alert: true,
6041
6607
  });
6042
6608
  return;
@@ -6044,7 +6610,7 @@ class TelegramTransport {
6044
6610
  const payload = await this.getLiveApprovalPayloadByKey(payloadKey);
6045
6611
  if (!payload) {
6046
6612
  await ctx.answerCallbackQuery({
6047
- text: "Запрос Live View устарел.",
6613
+ text: this.t(locale, "menu:project.approval_stale"),
6048
6614
  show_alert: true,
6049
6615
  });
6050
6616
  await ctx.deleteMessage().catch(() => undefined);
@@ -6068,28 +6634,45 @@ class TelegramTransport {
6068
6634
  },
6069
6635
  });
6070
6636
  await ctx.answerCallbackQuery({
6071
- 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"),
6072
6640
  });
6073
6641
  if (ctx.callbackQuery?.message) {
6074
6642
  await this.editText(ctx, [
6075
- approved ? "🖥 Live View разрешён" : "🖥 Live View отклонён",
6643
+ approved
6644
+ ? this.t(locale, "menu:live.approval.approved")
6645
+ : this.t(locale, "menu:live.approval.denied"),
6076
6646
  "",
6077
- ...(payload.projectName ? [`Проект: ${payload.projectName}`] : []),
6078
- `Сессия: ${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
+ }),
6079
6658
  "",
6080
6659
  result?.delivered
6081
6660
  ? approved
6082
- ? "Запросившая сторона получила кнопку для открытия Live."
6083
- : "Запросившая сторона получила уведомление об отклонении."
6084
- : "Запросившая сторона сейчас недоступна.",
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"),
6085
6667
  ].join("\n"), { kind: "menu", sessionId: payload.sessionId });
6086
6668
  }
6087
6669
  }
6088
6670
  async handleProjectMemberFilesCallback(ctx) {
6671
+ const locale = await this.resolveLocaleForContext(ctx);
6089
6672
  const payloadKey = this.extractCallbackSuffix(ctx, "project-member-files:");
6090
6673
  if (!payloadKey) {
6091
6674
  await ctx.answerCallbackQuery({
6092
- text: "Некорректные данные участника проекта.",
6675
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6093
6676
  show_alert: true,
6094
6677
  });
6095
6678
  return;
@@ -6097,20 +6680,23 @@ class TelegramTransport {
6097
6680
  const payload = await this.getProjectMemberPayloadByKey(payloadKey);
6098
6681
  if (!payload) {
6099
6682
  await ctx.answerCallbackQuery({
6100
- text: "Данные участника проекта некорректны или устарели.",
6683
+ text: this.t(locale, "menu:project.stale_member_payload"),
6101
6684
  show_alert: true,
6102
6685
  });
6103
6686
  await ctx.deleteMessage().catch(() => undefined);
6104
6687
  return;
6105
6688
  }
6106
- await ctx.answerCallbackQuery({ text: "Открываю файлы." });
6689
+ await ctx.answerCallbackQuery({
6690
+ text: this.t(locale, "menu:project.opening_files"),
6691
+ });
6107
6692
  await this.showProjectMemberFiles(ctx, payload);
6108
6693
  }
6109
6694
  async handlePartnerFileOpenCallback(ctx) {
6695
+ const locale = await this.resolveLocaleForContext(ctx);
6110
6696
  const payloadKey = this.extractCallbackSuffix(ctx, "partner-file-open:");
6111
6697
  if (!payloadKey) {
6112
6698
  await ctx.answerCallbackQuery({
6113
- text: "Некорректные данные файла.",
6699
+ text: this.t(locale, "menu:project.invalid_member_payload"),
6114
6700
  show_alert: true,
6115
6701
  });
6116
6702
  return;
@@ -6118,7 +6704,7 @@ class TelegramTransport {
6118
6704
  const payload = await this.getPartnerFileTargetPayloadByKey(payloadKey);
6119
6705
  if (!payload) {
6120
6706
  await ctx.answerCallbackQuery({
6121
- text: "Данные файла устарели.",
6707
+ text: this.t(locale, "menu:project.data_stale"),
6122
6708
  show_alert: true,
6123
6709
  });
6124
6710
  return;
@@ -6131,11 +6717,12 @@ class TelegramTransport {
6131
6717
  });
6132
6718
  }
6133
6719
  async handleProjectMembersCallback(ctx) {
6720
+ const locale = await this.resolveLocaleForContext(ctx);
6134
6721
  const projectUuid = this.extractCallbackSuffix(ctx, "project-members:");
6135
6722
  const principal = this.getPrincipalFromContext(ctx);
6136
6723
  if (!projectUuid || !principal) {
6137
6724
  await ctx.answerCallbackQuery({
6138
- text: "Некорректное действие проекта.",
6725
+ text: this.t(locale, "menu:project.invalid_action"),
6139
6726
  show_alert: true,
6140
6727
  });
6141
6728
  return;
@@ -6143,7 +6730,7 @@ class TelegramTransport {
6143
6730
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6144
6731
  if (!sessionId) {
6145
6732
  await ctx.answerCallbackQuery({
6146
- text: "Активная сессия не выбрана.",
6733
+ text: this.t(locale, "common:errors.no_active_session"),
6147
6734
  show_alert: true,
6148
6735
  });
6149
6736
  return;
@@ -6151,20 +6738,23 @@ class TelegramTransport {
6151
6738
  const payload = await this.getProjectPayloadByUuid(sessionId, projectUuid);
6152
6739
  if (!payload) {
6153
6740
  await ctx.answerCallbackQuery({
6154
- text: "Проект не найден.",
6741
+ text: this.t(locale, "menu:project.not_found"),
6155
6742
  show_alert: true,
6156
6743
  });
6157
6744
  return;
6158
6745
  }
6159
- await ctx.answerCallbackQuery({ text: "Загружаю участников." });
6746
+ await ctx.answerCallbackQuery({
6747
+ text: this.t(locale, "menu:project.loading_members"),
6748
+ });
6160
6749
  await this.showProjectMembers(ctx, payload);
6161
6750
  }
6162
6751
  async handleProjectLeaveCallback(ctx) {
6752
+ const locale = await this.resolveLocaleForContext(ctx);
6163
6753
  const projectUuid = this.extractCallbackSuffix(ctx, "project-leave:");
6164
6754
  const principal = this.getPrincipalFromContext(ctx);
6165
6755
  if (!projectUuid || !principal) {
6166
6756
  await ctx.answerCallbackQuery({
6167
- text: "Некорректное действие проекта.",
6757
+ text: this.t(locale, "menu:project.invalid_action"),
6168
6758
  show_alert: true,
6169
6759
  });
6170
6760
  return;
@@ -6172,7 +6762,7 @@ class TelegramTransport {
6172
6762
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6173
6763
  if (!sessionId) {
6174
6764
  await ctx.answerCallbackQuery({
6175
- text: "Активная сессия не выбрана.",
6765
+ text: this.t(locale, "common:errors.no_active_session"),
6176
6766
  show_alert: true,
6177
6767
  });
6178
6768
  return;
@@ -6191,14 +6781,17 @@ class TelegramTransport {
6191
6781
  updatedAt: new Date().toISOString(),
6192
6782
  });
6193
6783
  }
6194
- await ctx.answerCallbackQuery({ text: "Выход из проекта выполнен." });
6195
- 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"));
6196
6788
  }
6197
6789
  async handleProjectDeleteByUuid(ctx, projectUuid) {
6790
+ const locale = await this.resolveLocaleForContext(ctx);
6198
6791
  const principal = this.getPrincipalFromContext(ctx);
6199
6792
  if (!principal) {
6200
6793
  await ctx.answerCallbackQuery({
6201
- text: "Telegram identity is unavailable.",
6794
+ text: this.t(locale, "common:errors.no_telegram_identity"),
6202
6795
  show_alert: true,
6203
6796
  });
6204
6797
  return;
@@ -6206,7 +6799,7 @@ class TelegramTransport {
6206
6799
  const sessionId = await this.bindingStore.getActiveSessionIdForPrincipal(principal);
6207
6800
  if (!sessionId) {
6208
6801
  await ctx.answerCallbackQuery({
6209
- text: "Активная сессия не выбрана.",
6802
+ text: this.t(locale, "common:errors.no_active_session"),
6210
6803
  show_alert: true,
6211
6804
  });
6212
6805
  return;
@@ -6215,7 +6808,7 @@ class TelegramTransport {
6215
6808
  const project = projects.find((item) => item.project_uuid === projectUuid);
6216
6809
  if (!project) {
6217
6810
  await ctx.answerCallbackQuery({
6218
- text: "Проект не найден.",
6811
+ text: this.t(locale, "menu:project.not_found"),
6219
6812
  show_alert: true,
6220
6813
  });
6221
6814
  await ctx.deleteMessage().catch(() => undefined);
@@ -6223,7 +6816,7 @@ class TelegramTransport {
6223
6816
  }
6224
6817
  if (project.role !== "owner") {
6225
6818
  await ctx.answerCallbackQuery({
6226
- text: "Удалять проект может только owner.",
6819
+ text: this.t(locale, "menu:project.delete_only_owner"),
6227
6820
  show_alert: true,
6228
6821
  });
6229
6822
  return;
@@ -6242,8 +6835,12 @@ class TelegramTransport {
6242
6835
  updatedAt: new Date().toISOString(),
6243
6836
  });
6244
6837
  }
6245
- await ctx.answerCallbackQuery({ text: "Проект удалён." });
6246
- 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
+ }));
6247
6844
  }
6248
6845
  async deletePendingFileHandoffPrompt(ctx, pending) {
6249
6846
  if (!pending.promptMessageId) {
@@ -6262,10 +6859,11 @@ class TelegramTransport {
6262
6859
  }
6263
6860
  }
6264
6861
  async cancelPendingFileHandoff(ctx) {
6862
+ const locale = await this.resolveLocaleForContext(ctx);
6265
6863
  const principal = this.getPrincipalFromContext(ctx);
6266
6864
  if (!principal) {
6267
6865
  await ctx.answerCallbackQuery({
6268
- text: "Telegram identity is unavailable.",
6866
+ text: this.t(locale, "common:errors.no_telegram_identity"),
6269
6867
  show_alert: true,
6270
6868
  });
6271
6869
  return;
@@ -6274,14 +6872,16 @@ class TelegramTransport {
6274
6872
  const pending = this.pendingFileHandoffs.get(principalKey);
6275
6873
  if (!pending) {
6276
6874
  await ctx.answerCallbackQuery({
6277
- text: "No pending file handoff prompt.",
6875
+ text: this.t(locale, "menu:handoff.no_pending"),
6278
6876
  show_alert: true,
6279
6877
  });
6280
6878
  return;
6281
6879
  }
6282
6880
  this.pendingFileHandoffs.delete(principalKey);
6283
6881
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6284
- await ctx.answerCallbackQuery({ text: "File handoff cancelled." });
6882
+ await ctx.answerCallbackQuery({
6883
+ text: this.t(locale, "menu:handoff.cancelled"),
6884
+ });
6285
6885
  if (pending.projectUuid && pending.targetSessionId && pending.targetSessionLabel) {
6286
6886
  const project = await this.getProjectPayloadByUuid(pending.sessionId, pending.projectUuid);
6287
6887
  if (project) {
@@ -6425,6 +7025,7 @@ class TelegramTransport {
6425
7025
  });
6426
7026
  }
6427
7027
  async handlePendingPartnerNote(ctx, text) {
7028
+ const locale = await this.resolveLocaleForContext(ctx);
6428
7029
  const principal = this.getPrincipalFromContext(ctx);
6429
7030
  if (!principal) {
6430
7031
  return false;
@@ -6446,9 +7047,11 @@ class TelegramTransport {
6446
7047
  if (!resolvedTargetLabel && sourceSession?.linkedSessionId) {
6447
7048
  const linkedSession = await this.sessionStore.getSession(sourceSession.linkedSessionId);
6448
7049
  resolvedTargetLabel =
6449
- linkedSession?.label ?? sourceSession.linkedSessionId ?? "напарник";
7050
+ linkedSession?.label ??
7051
+ sourceSession.linkedSessionId ??
7052
+ this.t(locale, "menu:partner.screen.default_partner");
6450
7053
  }
6451
- const targetLabel = resolvedTargetLabel ?? "напарник";
7054
+ const targetLabel = resolvedTargetLabel ?? this.t(locale, "menu:partner.screen.default_partner");
6452
7055
  this.pendingPartnerNotes.delete(principalKey);
6453
7056
  await this.deletePendingPartnerNotePrompt(ctx, pending);
6454
7057
  if ((0, collabSemantics_1.isExecutorTargetKind)(pending.kind)) {
@@ -6484,13 +7087,30 @@ class TelegramTransport {
6484
7087
  requires_reply: true,
6485
7088
  });
6486
7089
  const sent = await this.replyText(ctx, [
6487
- "Задача отправлена выбранной сессии.",
7090
+ this.t(locale, "menu:partner.actions.task_sent"),
6488
7091
  ...(output.project_name ? [`Проект: ${output.project_name}`] : []),
6489
- ...(output.target_actor_label ? [`Исполнитель: ${output.target_actor_label}`] : []),
6490
- `Маршрут результата: ${targetLabel} -> ${sourceLabel}`,
6491
- `Тип: ${pending.kind}`,
6492
- `Кратко: ${parsed.summary}`,
6493
- `Статус: ${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
+ }),
6494
7114
  `Share: ${output.share_id}`,
6495
7115
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6496
7116
  if (output.delivery_status === "queued" &&
@@ -6532,11 +7152,18 @@ class TelegramTransport {
6532
7152
  ...(pending.projectUuid ? { projectUuid: pending.projectUuid } : {}),
6533
7153
  });
6534
7154
  await this.replyText(ctx, [
6535
- "Задача поставлена в inbox текущей сессии.",
6536
- `Маршрут отправки: ${sourceLabel} -> ${targetLabel}`,
6537
- `Тип: ${pending.kind}`,
6538
- `Кратко: ${parsed.summary}`,
6539
- "Текущая сессия подготовит результат и отправит его сама.",
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"),
6540
7167
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6541
7168
  return true;
6542
7169
  }
@@ -6580,6 +7207,7 @@ class TelegramTransport {
6580
7207
  await this.nudgeSessionInbox(input.sessionId);
6581
7208
  }
6582
7209
  async handlePendingFileHandoff(ctx, text) {
7210
+ const locale = await this.resolveLocaleForContext(ctx);
6583
7211
  const principal = this.getPrincipalFromContext(ctx);
6584
7212
  if (!principal) {
6585
7213
  return false;
@@ -6608,7 +7236,7 @@ class TelegramTransport {
6608
7236
  });
6609
7237
  this.pendingFileHandoffs.delete(principalKey);
6610
7238
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6611
- 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 });
6612
7240
  return true;
6613
7241
  }
6614
7242
  if (pending.projectUuid) {
@@ -6630,12 +7258,36 @@ class TelegramTransport {
6630
7258
  this.pendingFileHandoffs.delete(principalKey);
6631
7259
  await this.deletePendingFileHandoffPrompt(ctx, pending);
6632
7260
  const sent = await this.replyText(ctx, [
6633
- "Файл поставлен в очередь доставки напарнику.",
6634
- ...(output.project_name ? [`Проект: ${output.project_name}`] : []),
6635
- ...(output.target_actor_label ? [`Получатель: ${output.target_actor_label}`] : []),
6636
- ...(output.target_session_label ? [`Сессия: ${output.target_session_label}`] : []),
6637
- `Статус: ${output.delivery_status === "delivered" ? "доставлено" : "в очереди"}`,
6638
- `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
+ }),
6639
7291
  ].join("\n"), { kind: "menu", sessionId: pending.sessionId });
6640
7292
  if (output.delivery_status === "queued" &&
6641
7293
  sent &&
@@ -6671,6 +7323,7 @@ class TelegramTransport {
6671
7323
  return true;
6672
7324
  }
6673
7325
  async handlePendingProject(ctx, text) {
7326
+ const locale = await this.resolveLocaleForContext(ctx);
6674
7327
  const principal = this.getPrincipalFromContext(ctx);
6675
7328
  if (!principal) {
6676
7329
  return false;
@@ -6704,7 +7357,10 @@ class TelegramTransport {
6704
7357
  projectUuid,
6705
7358
  projectName,
6706
7359
  });
6707
- 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 });
6708
7364
  }
6709
7365
  else {
6710
7366
  const joined = await this.callGatewayJson("/projects/join", {
@@ -6719,10 +7375,14 @@ class TelegramTransport {
6719
7375
  projectUuid,
6720
7376
  projectName,
6721
7377
  });
6722
- 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 });
6723
7381
  }
6724
7382
  this.pendingProjects.delete(principalKey);
6725
- await this.showProjectsMenu(ctx, `Открыт проект: ${projectName}`);
7383
+ await this.showProjectsMenu(ctx, this.t(locale, "menu:project.opened", {
7384
+ projectName,
7385
+ }));
6726
7386
  return true;
6727
7387
  }
6728
7388
  }