diff --git a/src/__tests__/bot-commands.test.ts b/src/__tests__/bot-commands.test.ts
index e2720eb..0bda3ad 100644
--- a/src/__tests__/bot-commands.test.ts
+++ b/src/__tests__/bot-commands.test.ts
@@ -22,18 +22,6 @@ describe("parseCommand", () => {
it("rejects /launch without slug", () => {
expect(parseCommand("/launch").tag).toBe("launch-missing");
});
- it("parses /lang without arg as lang-help", () => {
- expect(parseCommand("/lang")).toEqual({ tag: "lang-help" });
- });
- it("parses /lang en", () => {
- expect(parseCommand("/lang en")).toEqual({ tag: "lang-set", locale: "en" });
- });
- it("parses /lang ru", () => {
- expect(parseCommand("/lang ru")).toEqual({ tag: "lang-set", locale: "ru" });
- });
- it("parses unknown /lang code as lang-invalid", () => {
- expect(parseCommand("/lang de")).toEqual({ tag: "lang-invalid", arg: "de" });
- });
it("parses /getToken", () => {
expect(parseCommand("/getToken")).toEqual({ tag: "get-token" });
});
@@ -50,28 +38,8 @@ describe("parseCommand", () => {
});
describe("bot messages", () => {
- it("createdMessage (ru) includes bootstrap steps without links", () => {
- const message = createdMessage(
- "ru",
- "demo-app",
- "npx @spawn-dock/create --token pair_demo",
- );
-
- expect(message).toContain("Проект demo-app создан.");
- expect(message).toContain("1. Запусти bootstrap-команду локально:");
- expect(message).toContain("npx @spawn-dock/create --token pair_demo");
- expect(message).toContain("Эту команду можно запускать повторно для этого проекта.");
- expect(message).toContain("2. После bootstrap запусти:");
- expect(message).toContain("pnpm run dev");
- expect(message).not.toContain("Preview URL:");
- expect(message).not.toContain("Telegram Link:");
- expect(message).not.toContain("TMA URL:");
- expect(message).not.toContain("Ссылки:");
- });
-
- it("createdMessage (en) uses English bootstrap copy", () => {
+ it("createdMessage uses English bootstrap copy", () => {
const message = createdMessage(
- "en",
"demo-app",
"npx @spawn-dock/create --token pair_demo",
);
@@ -86,7 +54,6 @@ describe("bot messages", () => {
it("includes TMA, preview, and telegram links in launchMessage", () => {
const message = launchMessage(
- "en",
"demo-app",
"connected",
"https://example.com/tma?tgWebAppStartParam=demo-app",
@@ -103,7 +70,6 @@ describe("bot messages", () => {
it("previewReadyMessage keeps all links", () => {
const message = previewReadyMessage(
- "ru",
"demo-app",
"https://example.com/preview/demo-app",
"https://t.me/rustgpt_bot/tma?startapp=demo-app",
diff --git a/src/__tests__/bot-telegram.test.ts b/src/__tests__/bot-telegram.test.ts
index a7c1f99..33287e1 100644
--- a/src/__tests__/bot-telegram.test.ts
+++ b/src/__tests__/bot-telegram.test.ts
@@ -71,6 +71,8 @@ describe("setMyCommands", () => {
for (const cmd of body.commands) {
expect(cmd.command).toMatch(/^[a-z0-9_]+$/);
}
- expect(body.commands.map((c) => c.command)).toContain("gettoken");
+ const commandNames = body.commands.map((c) => c.command);
+ expect(commandNames).toContain("gettoken");
+ expect(commandNames).not.toContain("lang");
});
});
diff --git a/src/__tests__/mcp-auth.test.ts b/src/__tests__/mcp-auth.test.ts
index 3db819c..4871ad2 100644
--- a/src/__tests__/mcp-auth.test.ts
+++ b/src/__tests__/mcp-auth.test.ts
@@ -17,7 +17,6 @@ const state: StoreState = {
revokedAt: null,
}],
tunnelSessions: [],
- userLocales: {},
};
describe("readMcpApiKey", () => {
diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts
index 3b49f57..8650b1c 100644
--- a/src/__tests__/server.test.ts
+++ b/src/__tests__/server.test.ts
@@ -27,7 +27,7 @@ process.env.TELEGRAM_MINI_APP_SHORT_NAME ??= "tma";
function createRuntime(): Runtime {
return {
- state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} },
+ state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] },
connectionsBySlug: new Map(),
pendingResponses: new Map(),
};
@@ -49,7 +49,6 @@ function createAuthorizedRuntime(): Runtime {
revokedAt: null,
}],
tunnelSessions: [],
- userLocales: {},
},
};
}
@@ -59,7 +58,7 @@ const authorizedHeaders = {
};
const mockRuntime: Runtime = {
- state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} },
+ state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] },
connectionsBySlug: new Map(),
pendingResponses: new Map(),
};
@@ -116,7 +115,7 @@ describe("Express server", () => {
it("POST /v1/bootstrap/claim returns flat bootstrap fields", async () => {
const app = createApp({
- state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} },
+ state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] },
connectionsBySlug: new Map(),
pendingResponses: new Map(),
});
@@ -172,7 +171,7 @@ describe("Express server", () => {
it("POST /v1/bootstrap/claim replays the same credential when the token was already claimed", async () => {
const app = createApp({
- state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} },
+ state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] },
connectionsBySlug: new Map(),
pendingResponses: new Map(),
});
@@ -209,35 +208,6 @@ describe("Express server", () => {
expect(res.body.error).toBe("bot_unauthorized");
});
- it("POST /api/bot/user-locale/sync stores locale from Telegram language_code", async () => {
- const runtime = createRuntime();
- const app = createApp(runtime);
-
- const res = await request(app)
- .post("/api/bot/user-locale/sync")
- .set(botHeaders)
- .send({ ownerTelegramId: 99, telegramLanguageCode: "ru-RU" });
-
- expect(res.status).toBe(200);
- expect(res.body.locale).toBe("ru");
- expect(runtime.state.userLocales["99"]).toBe("ru");
- });
-
- it("POST /api/bot/user-locale/set updates explicit locale", async () => {
- const runtime = createRuntime();
- runtime.state.userLocales["5"] = "ru";
- const app = createApp(runtime);
-
- const res = await request(app)
- .post("/api/bot/user-locale/set")
- .set(botHeaders)
- .send({ ownerTelegramId: 5, locale: "en" });
-
- expect(res.status).toBe(200);
- expect(res.body.locale).toBe("en");
- expect(runtime.state.userLocales["5"]).toBe("en");
- });
-
it("POST /projects accepts bot-authorized creation without /api prefix", async () => {
const app = createApp(createRuntime());
@@ -314,7 +284,6 @@ describe("Express server", () => {
pairingTokens: [],
deviceCredentials: [],
tunnelSessions: [],
- userLocales: {},
},
connectionsBySlug: new Map(),
pendingResponses: new Map(),
diff --git a/src/bot/commands.ts b/src/bot/commands.ts
index 9de0ba5..c8fa9d2 100644
--- a/src/bot/commands.ts
+++ b/src/bot/commands.ts
@@ -1,16 +1,10 @@
// src/bot/commands.ts
-import type { BotLocale } from "../types.js";
-import { isBotLocale } from "./user-lang.js";
-
export type BotCommand =
| { tag: "start" }
| { tag: "help" }
| { tag: "new"; title: string }
| { tag: "launch"; slug: string }
| { tag: "launch-missing" }
- | { tag: "lang-help" }
- | { tag: "lang-set"; locale: BotLocale }
- | { tag: "lang-invalid"; arg: string }
| { tag: "get-token" }
| { tag: "unknown"; input: string };
@@ -30,12 +24,6 @@ export function parseCommand(input: string): BotCommand {
const text = normalizeTelegramCommandText(input);
if (text === "/start") return { tag: "start" };
if (text === "/help") return { tag: "help" };
- if (text.startsWith("/lang")) {
- const rest = text.slice(5).trim().toLowerCase();
- if (!rest) return { tag: "lang-help" };
- if (isBotLocale(rest)) return { tag: "lang-set", locale: rest };
- return { tag: "lang-invalid", arg: rest };
- }
if (text.startsWith("/new")) {
const title = text.slice(4).trim();
return { tag: "new", title: title || "SpawnDock App" };
diff --git a/src/bot/i18n.ts b/src/bot/i18n.ts
index ca2a870..47e9efb 100644
--- a/src/bot/i18n.ts
+++ b/src/bot/i18n.ts
@@ -1,6 +1,4 @@
// src/bot/i18n.ts
-import type { BotLocale } from "../types.js";
-
const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">");
const escAttr = (s: string) => s
.replace(/&/g, "&")
@@ -12,18 +10,11 @@ function renderLink(url: string): string {
return `${esc(url)}`;
}
-export function linkButtonLabels(locale: BotLocale): {
+export function linkButtonLabels(): {
openTma: string;
openPreview: string;
openTelegramLink: string;
} {
- if (locale === "ru") {
- return {
- openTma: "Открыть TMA",
- openPreview: "Открыть Preview",
- openTelegramLink: "Открыть Telegram Link",
- };
- }
return {
openTma: "Open TMA",
openPreview: "Open Preview",
@@ -31,19 +22,7 @@ export function linkButtonLabels(locale: BotLocale): {
};
}
-export function welcomeMessage(locale: BotLocale): string {
- if (locale === "ru") {
- return [
- "SpawnDock бот готов.",
- "",
- "Команды:",
- "/new <название проекта> — создать проект и получить ссылку (или команду) для spawn-dock",
- "/launch <slug> — получить TMA и preview ссылки проекта",
- "/gettoken — показать общий API_TOKEN для MCP и dev tunnel",
- "/lang <en|ru> — язык интерфейса",
- "/help — показать справку",
- ].join("\n");
- }
+export function welcomeMessage(): string {
return [
"SpawnDock bot is ready.",
"",
@@ -51,26 +30,13 @@ export function welcomeMessage(locale: BotLocale): string {
"/new <project title> — create a project and get the spawn-dock link (or command)",
"/launch <slug> — get TMA and preview links for the project",
"/gettoken — show shared API_TOKEN for MCP and dev tunnel",
- "/lang <en|ru> — interface language",
"/help — show this help",
].join("\n");
}
-export function getTokenMessage(locale: BotLocale, token: string): string {
+export function getTokenMessage(token: string): string {
if (token.length === 0) {
- return locale === "ru"
- ? "API_TOKEN не настроен на сервере."
- : "API_TOKEN is not configured on the server.";
- }
-
- if (locale === "ru") {
- return [
- "Общий токен для MCP и dev tunnel:",
- `${esc(token)}`,
- "",
- "Пример:",
- "export API_TOKEN=...",
- ].join("\n");
+ return "API_TOKEN is not configured on the server.";
}
return [
@@ -82,38 +48,18 @@ export function getTokenMessage(locale: BotLocale, token: string): string {
].join("\n");
}
-export function unknownMessage(locale: BotLocale): string {
- if (locale === "ru") {
- return "Не понял команду.\n\nИспользуй /new, /launch, /gettoken или /help.";
- }
+export function unknownMessage(): string {
return "I did not understand that command.\n\nUse /new, /launch, /gettoken, or /help.";
}
-export function launchUsageMessage(locale: BotLocale): string {
- if (locale === "ru") {
- return "Используй /launch <slug>, чтобы получить preview URL и текущий статус туннеля.";
- }
+export function launchUsageMessage(): string {
return "Use /launch <slug> to get the preview URL and current tunnel status.";
}
export function createdMessage(
- locale: BotLocale,
slug: string,
bootstrapCmd: string,
): string {
- if (locale === "ru") {
- return [
- `Проект ${esc(slug)} создан.`,
- "",
- "1. Запусти bootstrap-команду локально:",
- `${esc(bootstrapCmd)}`,
- "",
- "Эту команду можно запускать повторно для этого проекта.",
- "",
- "2. После bootstrap запусти:",
- "pnpm run dev",
- ].join("\n");
- }
return [
`Project ${esc(slug)} created.`,
"",
@@ -128,20 +74,17 @@ export function createdMessage(
}
export function launchMessage(
- locale: BotLocale,
slug: string,
status: "connected" | "offline",
tmaUrl: string,
previewUrl: string,
telegramMiniAppUrl: string,
): string {
- const statusLabel = locale === "ru" ? "Статус туннеля" : "Tunnel status";
- const offlineHint = locale === "ru"
- ? "Если preview офлайн, запусти npm run agent в каталоге проекта."
- : "If preview is offline, run npm run agent in the project directory.";
+ const statusLabel = "Tunnel status";
+ const offlineHint = "If preview is offline, run npm run agent in the project directory.";
return [
- locale === "ru" ? `Проект ${esc(slug)}.` : `Project ${esc(slug)}.`,
+ `Project ${esc(slug)}.`,
"",
`${statusLabel}: ${esc(status)}`,
`TMA URL: ${renderLink(tmaUrl)}`,
@@ -152,87 +95,32 @@ export function launchMessage(
].join("\n");
}
-export function ackCreatingProject(locale: BotLocale): string {
- return locale === "ru"
- ? "Создаю проект и готовлю ссылку на рабочее окружение..."
- : "Creating the project and preparing the workspace link...";
-}
-
-export function ackCheckingLaunch(locale: BotLocale): string {
- return locale === "ru"
- ? "Проверяю текущий статус проекта..."
- : "Checking the current project status...";
-}
-
-export function errCreateProject(locale: BotLocale): string {
- return locale === "ru"
- ? "Не удалось создать проект. Убедись, что сервер запущен, и попробуй ещё раз."
- : "Could not create the project. Make sure the server is running and try again.";
+export function ackCreatingProject(): string {
+ return "Creating the project and preparing the workspace link...";
}
-export function errLaunchNotFound(locale: BotLocale): string {
- return locale === "ru"
- ? "Проект не найден. Проверь slug и попробуй ещё раз."
- : "Project not found. Check the slug and try again.";
+export function ackCheckingLaunch(): string {
+ return "Checking the current project status...";
}
-export function errLaunchGeneric(locale: BotLocale): string {
- return locale === "ru"
- ? "Не удалось получить статус проекта. Попробуй позже."
- : "Could not load project status. Try again later.";
+export function errCreateProject(): string {
+ return "Could not create the project. Make sure the server is running and try again.";
}
-export function errLocaleSet(locale: BotLocale): string {
- return locale === "ru"
- ? "Не удалось сохранить язык. Попробуй позже."
- : "Could not save language preference. Try again later.";
+export function errLaunchNotFound(): string {
+ return "Project not found. Check the slug and try again.";
}
-export function langHelpMessage(locale: BotLocale): string {
- if (locale === "ru") {
- return [
- "Текущий язык можно сменить так:",
- "/lang en — English",
- "/lang ru — русский",
- ].join("\n");
- }
- return [
- "Change language with:",
- "/lang en — English",
- "/lang ru — русский",
- ].join("\n");
-}
-
-export function langSetConfirm(selectedLocale: BotLocale): string {
- if (selectedLocale === "ru") {
- return "Язык установлен: русский.";
- }
- return "Language set to English.";
-}
-
-export function langInvalid(locale: BotLocale, arg: string): string {
- if (locale === "ru") {
- return `Неизвестный язык «${esc(arg)}». Доступны: en, ru.`;
- }
- return `Unknown language «${esc(arg)}». Supported: en, ru.`;
+export function errLaunchGeneric(): string {
+ return "Could not load project status. Try again later.";
}
export function previewReadyMessage(
- locale: BotLocale,
slug: string,
previewUrl: string,
telegramMiniAppUrl: string,
tmaUrl: string,
): string {
- if (locale === "ru") {
- return [
- `Проект ${esc(slug)}: локальный сервер подключён, превью доступно.`,
- "",
- `Preview URL: ${renderLink(previewUrl)}`,
- `Telegram Link: ${renderLink(telegramMiniAppUrl)}`,
- `TMA URL: ${renderLink(tmaUrl)}`,
- ].join("\n");
- }
return [
`Project ${esc(slug)}: local server is connected; preview is available.`,
"",
diff --git a/src/bot/polling.ts b/src/bot/polling.ts
index 7756251..6c8bf07 100644
--- a/src/bot/polling.ts
+++ b/src/bot/polling.ts
@@ -1,5 +1,4 @@
// src/bot/polling.ts
-import type { BotLocale } from "../types.js";
import { readBotConfig } from "./config.js";
import { getUpdates, sendMessage, deleteWebhook, setMyCommands, type TelegramUpdate } from "./telegram.js";
import {
@@ -15,11 +14,7 @@ import {
ackCreatingProject,
errCreateProject,
errLaunchGeneric,
- errLocaleSet,
errLaunchNotFound,
- langHelpMessage,
- langInvalid,
- langSetConfirm,
linkButtonLabels,
getTokenMessage,
} from "./i18n.js";
@@ -55,104 +50,30 @@ async function getLaunchInfo(controlPlaneUrl: string, botSecret: string, ownerTe
return (await res.json()) as any;
}
-async function syncUserLocale(
- controlPlaneUrl: string,
- botSecret: string,
- ownerTelegramId: number,
- telegramLanguageCode?: string,
-): Promise {
- const res = await fetch(`${controlPlaneUrl}/api/bot/user-locale/sync`, {
- method: "POST",
- headers: {
- "content-type": "application/json",
- "x-spawndock-bot-secret": botSecret,
- },
- body: JSON.stringify({ ownerTelegramId, telegramLanguageCode }),
- });
- if (!res.ok) throw new Error(`Locale sync failed: ${res.status}`);
- const data = (await res.json()) as { locale: BotLocale };
- return data.locale;
-}
-
-async function setUserLocale(
- controlPlaneUrl: string,
- botSecret: string,
- ownerTelegramId: number,
- locale: BotLocale,
-): Promise {
- const res = await fetch(`${controlPlaneUrl}/api/bot/user-locale/set`, {
- method: "POST",
- headers: {
- "content-type": "application/json",
- "x-spawndock-bot-secret": botSecret,
- },
- body: JSON.stringify({ ownerTelegramId, locale }),
- });
- if (!res.ok) throw new Error(`Locale set failed: ${res.status}`);
- const data = (await res.json()) as { locale: BotLocale };
- return data.locale;
-}
-
-async function resolveLocale(
- cfg: ReturnType,
- from: { id: number; language_code?: string },
-): Promise {
- try {
- return await syncUserLocale(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, from.id, from.language_code);
- } catch {
- return "en";
- }
-}
-
async function processUpdate(cfg: ReturnType, update: TelegramUpdate): Promise {
const msg = update.message;
if (!msg?.text || !msg.from) return;
const cmd = parseCommand(msg.text);
- if (cmd.tag === "lang-set") {
- let selected: BotLocale;
- try {
- selected = await setUserLocale(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.locale);
- } catch {
- const locale = await resolveLocale(cfg, msg.from);
- await sendMessage(cfg.telegramBotToken, msg.chat.id, errLocaleSet(locale));
- return;
- }
- await sendMessage(cfg.telegramBotToken, msg.chat.id, langSetConfirm(selected));
- return;
- }
-
- const locale = await resolveLocale(cfg, msg.from);
-
- if (cmd.tag === "lang-help") {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, langHelpMessage(locale));
- return;
- }
-
- if (cmd.tag === "lang-invalid") {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, langInvalid(locale, cmd.arg));
- return;
- }
-
if (cmd.tag === "start" || cmd.tag === "help") {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, welcomeMessage(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, welcomeMessage());
return;
}
if (cmd.tag === "get-token") {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, getTokenMessage(locale, cfg.apiToken));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, getTokenMessage(cfg.apiToken));
return;
}
if (cmd.tag === "new") {
let data: any;
- const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCreatingProject(locale));
+ const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCreatingProject());
try {
data = await createProjectViaApi(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.title);
} catch (err: any) {
clearTimeout(ackTimeout);
- await sendMessage(cfg.telegramBotToken, msg.chat.id, errCreateProject(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, errCreateProject());
return;
}
clearTimeout(ackTimeout);
@@ -162,45 +83,45 @@ async function processUpdate(cfg: ReturnType, update: Tele
await sendMessage(
cfg.telegramBotToken,
msg.chat.id,
- createdMessage(locale, slug, bootstrapCmd),
+ createdMessage(slug, bootstrapCmd),
);
return;
}
if (cmd.tag === "launch-missing") {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, launchUsageMessage(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, launchUsageMessage());
return;
}
if (cmd.tag === "launch") {
let data: any;
- const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCheckingLaunch(locale));
+ const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCheckingLaunch());
try {
data = await getLaunchInfo(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.slug);
} catch (err: any) {
clearTimeout(ackTimeout);
if (String(err.message).includes("404")) {
- await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchNotFound(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchNotFound());
return;
}
- await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchGeneric(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchGeneric());
return;
}
clearTimeout(ackTimeout);
const previewUrl = data.launchUrl;
const tmaUrl = buildGatewayMiniAppUrl(previewUrl, cmd.slug);
const telegramMiniAppUrl = buildTelegramMiniAppUrl(cfg.telegramBotUsername, cfg.telegramMiniAppShortName, cmd.slug);
- const buttons = linkButtonLabels(locale);
+ const buttons = linkButtonLabels();
await sendMessage(
cfg.telegramBotToken,
msg.chat.id,
- launchMessage(locale, cmd.slug, data.status, tmaUrl, previewUrl, telegramMiniAppUrl),
+ launchMessage(cmd.slug, data.status, tmaUrl, previewUrl, telegramMiniAppUrl),
{ tmaUrl, previewUrl, telegramMiniAppUrl, buttonLabels: buttons },
);
return;
}
- await sendMessage(cfg.telegramBotToken, msg.chat.id, unknownMessage(locale));
+ await sendMessage(cfg.telegramBotToken, msg.chat.id, unknownMessage());
}
function scheduleAcknowledgement(
diff --git a/src/bot/preview-notify.ts b/src/bot/preview-notify.ts
index 32facbe..713071c 100644
--- a/src/bot/preview-notify.ts
+++ b/src/bot/preview-notify.ts
@@ -1,18 +1,15 @@
// src/bot/preview-notify.ts
-import type { StoreState } from "../types.js";
import { buildLaunchUrl } from "../projects.js";
import { buildGatewayMiniAppUrl, buildTelegramMiniAppUrl } from "./links.js";
import { sendMessage } from "./telegram.js";
import { config } from "../config.js";
-import { getUserLocale } from "./user-lang.js";
-import { linkButtonLabels, previewReadyMessage } from "./i18n.js";
+import { previewReadyMessage } from "./i18n.js";
/**
* When the control plane has Telegram credentials, notify the project owner that preview is reachable.
* Best-effort: failures are logged only.
*/
export async function notifyOwnerPreviewReady(input: {
- state: StoreState;
ownerTelegramId: number;
slug: string;
}): Promise {
@@ -23,17 +20,15 @@ export async function notifyOwnerPreviewReady(input: {
return;
}
- const locale = getUserLocale(input.state, input.ownerTelegramId);
const previewUrl = buildLaunchUrl(config.publicOrigin, input.slug, config.previewPrefix);
const tmaUrl = buildGatewayMiniAppUrl(previewUrl, input.slug);
const telegramMiniAppUrl = buildTelegramMiniAppUrl(botUsername, miniAppShortName, input.slug);
- const html = previewReadyMessage(locale, input.slug, previewUrl, telegramMiniAppUrl, tmaUrl);
+ const html = previewReadyMessage(input.slug, previewUrl, telegramMiniAppUrl, tmaUrl);
await sendMessage(token, input.ownerTelegramId, html, {
tmaUrl,
previewUrl,
telegramMiniAppUrl,
- buttonLabels: linkButtonLabels(locale),
});
}
diff --git a/src/bot/telegram.ts b/src/bot/telegram.ts
index 0c4f66c..412a188 100644
--- a/src/bot/telegram.ts
+++ b/src/bot/telegram.ts
@@ -207,7 +207,6 @@ export async function setMyCommands(token: string): Promise {
{ command: "new", description: "Create TMA and get bootstrap command" },
{ command: "launch", description: "Get TMA and preview links for a project" },
{ command: "gettoken", description: "Shared API token for MCP and dev tunnel" },
- { command: "lang", description: "Set language (en or ru)" },
{ command: "help", description: "Show help" },
],
}),
diff --git a/src/bot/user-lang.ts b/src/bot/user-lang.ts
deleted file mode 100644
index 4e054e0..0000000
--- a/src/bot/user-lang.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// src/bot/user-lang.ts
-import type { BotLocale, StoreState } from "../types.js";
-
-export const SUPPORTED_BOT_LOCALES: readonly BotLocale[] = ["en", "ru"];
-
-export function detectLocaleFromTelegram(languageCode?: string): BotLocale {
- if (!languageCode) return "en";
- const normalized = languageCode.toLowerCase().split("-")[0]?.trim() ?? "";
- return normalized === "ru" ? "ru" : "en";
-}
-
-export function isBotLocale(value: string): value is BotLocale {
- return value === "en" || value === "ru";
-}
-
-export function getUserLocale(state: StoreState, telegramUserId: number): BotLocale {
- return state.userLocales[String(telegramUserId)] ?? "en";
-}
-
-export function ensureUserLocale(
- state: StoreState,
- telegramUserId: number,
- telegramLanguageCode?: string,
-): { state: StoreState; locale: BotLocale; changed: boolean } {
- const key = String(telegramUserId);
- const existing = state.userLocales[key];
- if (existing) {
- return { state, locale: existing, changed: false };
- }
- const locale = detectLocaleFromTelegram(telegramLanguageCode);
- return {
- state: {
- ...state,
- userLocales: { ...state.userLocales, [key]: locale },
- },
- locale,
- changed: true,
- };
-}
-
-export function setUserLocaleInState(
- state: StoreState,
- telegramUserId: number,
- locale: BotLocale,
-): StoreState {
- return {
- ...state,
- userLocales: { ...state.userLocales, [String(telegramUserId)]: locale },
- };
-}
diff --git a/src/routes/bot-user.ts b/src/routes/bot-user.ts
deleted file mode 100644
index 94fdfb1..0000000
--- a/src/routes/bot-user.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-// src/routes/bot-user.ts
-import { Router, type RequestHandler } from "express";
-import type { BotLocale, Runtime } from "../types.js";
-import { saveState } from "../store.js";
-import { isAuthorizedBotRequest } from "./bot-auth.js";
-import { ensureUserLocale, isBotLocale, setUserLocaleInState } from "../bot/user-lang.js";
-
-export function botUserRoutes(runtime: Runtime, stateFilePath: string): Router {
- const router = Router();
-
- const handleSync: RequestHandler = (req, res) => {
- if (!isAuthorizedBotRequest(req.headers)) {
- res.status(401).json({ error: "bot_unauthorized" });
- return;
- }
-
- const { ownerTelegramId, telegramLanguageCode } = req.body ?? {};
- if (typeof ownerTelegramId !== "number" || !Number.isFinite(ownerTelegramId)) {
- res.status(400).json({ error: "ownerTelegramId is required" });
- return;
- }
-
- const telegramLang =
- typeof telegramLanguageCode === "string" && telegramLanguageCode.length > 0
- ? telegramLanguageCode
- : undefined;
-
- const result = ensureUserLocale(runtime.state, ownerTelegramId, telegramLang);
- if (result.changed) {
- runtime.state = result.state;
- saveState(stateFilePath, runtime.state);
- }
-
- res.json({ locale: result.locale });
- };
-
- const handleSet: RequestHandler = (req, res) => {
- if (!isAuthorizedBotRequest(req.headers)) {
- res.status(401).json({ error: "bot_unauthorized" });
- return;
- }
-
- const { ownerTelegramId, locale } = req.body ?? {};
- if (typeof ownerTelegramId !== "number" || !Number.isFinite(ownerTelegramId)) {
- res.status(400).json({ error: "ownerTelegramId is required" });
- return;
- }
-
- if (typeof locale !== "string" || !isBotLocale(locale)) {
- res.status(400).json({ error: "locale must be en or ru" });
- return;
- }
-
- runtime.state = setUserLocaleInState(runtime.state, ownerTelegramId, locale as BotLocale);
- saveState(stateFilePath, runtime.state);
-
- res.json({ locale });
- };
-
- router.post("/api/bot/user-locale/sync", handleSync);
- router.post("/api/bot/user-locale/set", handleSet);
-
- return router;
-}
diff --git a/src/server.ts b/src/server.ts
index 18310c9..89213e1 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -10,7 +10,6 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { createMcpServer } from "./mcp.js";
import { config } from "./config.js";
import { projectRoutes } from "./routes/projects.js";
-import { botUserRoutes } from "./routes/bot-user.js";
import { previewRoutes } from "./routes/preview.js";
import { tmaRoutes } from "./routes/tma.js";
import { authenticateMcpRequest } from "./mcp-auth.js";
@@ -140,7 +139,6 @@ export function createApp(runtime: Runtime): Application {
return next();
});
app.use(projectRoutes(runtime, config.stateFile));
- app.use(botUserRoutes(runtime, config.stateFile));
app.use(tmaRoutes());
// Preview proxy — MUST be mounted WITHOUT express.json() to read raw body stream
diff --git a/src/store.ts b/src/store.ts
index 5734609..e50368b 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -8,17 +8,18 @@ export const emptyState = (): StoreState => ({
pairingTokens: [],
deviceCredentials: [],
tunnelSessions: [],
- userLocales: {},
});
export function loadState(filePath: string): StoreState {
try {
const data = readFileSync(filePath, "utf-8");
- const parsed = JSON.parse(data) as StoreState;
- if (!parsed.userLocales || typeof parsed.userLocales !== "object") {
- parsed.userLocales = {};
- }
- return parsed;
+ const parsed = JSON.parse(data) as Partial & Record;
+ return {
+ projects: Array.isArray(parsed.projects) ? parsed.projects : [],
+ pairingTokens: Array.isArray(parsed.pairingTokens) ? parsed.pairingTokens : [],
+ deviceCredentials: Array.isArray(parsed.deviceCredentials) ? parsed.deviceCredentials : [],
+ tunnelSessions: Array.isArray(parsed.tunnelSessions) ? parsed.tunnelSessions : [],
+ };
} catch {
return emptyState();
}
diff --git a/src/tunnel.ts b/src/tunnel.ts
index c6600cd..6946e06 100644
--- a/src/tunnel.ts
+++ b/src/tunnel.ts
@@ -139,7 +139,6 @@ function handleTunnelConnection(
if (!previousConnection && result.project.ownerTelegramId != null) {
void notifyOwnerPreviewReady({
- state: runtime.state,
ownerTelegramId: result.project.ownerTelegramId,
slug: result.project.slug,
}).catch((err) => {
diff --git a/src/types.ts b/src/types.ts
index 56e5e47..763b2ec 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,4 @@
// src/types.ts
-export type BotLocale = "en" | "ru";
-
export type ProjectStatus = "draft" | "active";
export type TunnelStatus = "connected" | "disconnected";
@@ -48,8 +46,6 @@ export interface StoreState {
pairingTokens: PairingToken[];
deviceCredentials: DeviceCredential[];
tunnelSessions: TunnelSession[];
- /** Telegram user id (string) → preferred bot locale */
- userLocales: Record;
}
export interface Runtime {