From 6b1708bf9066c841060b76b090621d3d923ab19a Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Sat, 21 Feb 2026 13:59:30 -0800 Subject: [PATCH 1/3] Add translation service --- routes/ai/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/routes/ai/index.ts b/routes/ai/index.ts index 726ff9b..d97924b 100644 --- a/routes/ai/index.ts +++ b/routes/ai/index.ts @@ -3,6 +3,8 @@ import { summarizeComments } from "./summarizeComments"; import { filterPosts } from "./filterPosts"; import { createEmbedding } from "./createEmbedding"; import { askQuestion } from "./askQuestion"; +import { translatePost } from "./translatePost"; +import { translateComment } from "./translateComments"; export default { "/api/ai/summarizePostDetails": { @@ -11,6 +13,12 @@ export default { "/api/ai/summarizeComments": { POST: summarizeComments, }, + "/api/ai/translatePost": { + POST: translatePost, + }, + "/api/ai/translateComment": { + POST: translateComment, + }, "/api/ai/filterPosts": { POST: filterPosts, }, From 0c31519007c2d6223560a63962f4f8598788c9e3 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Sat, 21 Feb 2026 14:03:13 -0800 Subject: [PATCH 2/3] Translation service components --- routes/ai/index.ts | 2 +- routes/ai/translateComment.ts | 96 ++++++++++++++++++++++++++++++++ routes/ai/translatePost.ts | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 routes/ai/translateComment.ts create mode 100644 routes/ai/translatePost.ts diff --git a/routes/ai/index.ts b/routes/ai/index.ts index d97924b..fc4ede1 100644 --- a/routes/ai/index.ts +++ b/routes/ai/index.ts @@ -4,7 +4,7 @@ import { filterPosts } from "./filterPosts"; import { createEmbedding } from "./createEmbedding"; import { askQuestion } from "./askQuestion"; import { translatePost } from "./translatePost"; -import { translateComment } from "./translateComments"; +import { translateComment } from "./translateComment"; export default { "/api/ai/summarizePostDetails": { diff --git a/routes/ai/translateComment.ts b/routes/ai/translateComment.ts new file mode 100644 index 0000000..46eae14 --- /dev/null +++ b/routes/ai/translateComment.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; +import { verifySubscription } from "../../middleware/subscription"; +import { aiClient } from "../../utils/models"; +import { ai_provider } from "../../utils/models"; +import { generateText } from "ai"; +import { AIUsage } from "../../services/AIUsage"; + +const translateCommentSchema = z.object({ + customerId: z.string(), + comment: z.string(), + sourceLanguage: z.string(), + targetLanguage: z.string(), +}); + +const systemPrompt = `You are a helpful assistant that translates text. Translate the given text accurately while preserving the original meaning, tone, and context. Only provide the translation without any additional explanations or formatting.`; + +const makeUserPrompt = ( + sourceLanguage: string, + targetLanguage: string, + comment: string, +) => { + const sourceLangName = sourceLanguage === "auto" ? "automatically detected language" : + sourceLanguage === "en" ? "English" : + sourceLanguage === "zh-Hant" ? "Chinese Traditional" : + sourceLanguage === "zh-Hans" ? "Chinese Simplified" : + sourceLanguage === "es" ? "Spanish" : + sourceLanguage === "fr" ? "French" : + sourceLanguage === "de" ? "German" : + sourceLanguage === "pt" ? "Portuguese" : + sourceLanguage === "ja" ? "Japanese" : + sourceLanguage === "ko" ? "Korean" : + sourceLanguage === "ru" ? "Russian" : sourceLanguage; + + const targetLangName = + targetLanguage === "en" ? "English" : + targetLanguage === "zh-Hant" ? "Chinese Traditional" : + targetLanguage === "zh-Hans" ? "Chinese Simplified" : + targetLanguage === "es" ? "Spanish" : + targetLanguage === "fr" ? "French" : + targetLanguage === "de" ? "German" : + targetLanguage === "pt" ? "Portuguese" : + targetLanguage === "ja" ? "Japanese" : + targetLanguage === "ko" ? "Korean" : + targetLanguage === "ru" ? "Russian" : targetLanguage; + + return ` +Translate the following comment from ${sourceLangName} to ${targetLangName}: + +${comment} +`; +}; + +let MODEL_ID: string = ""; + +if (ai_provider == "groq") { + MODEL_ID = "openai/gpt-oss-20b"; +} else { + MODEL_ID = process.env.OPENAI_SUMMARY_MODEL || "gpt-4.1-mini"; +} + +export async function translateComment(req: Request) { + const body = await req.json(); + const { customerId, comment, sourceLanguage, targetLanguage } = + translateCommentSchema.parse(body); + const isSubscribed = await verifySubscription(customerId); + if (!isSubscribed) { + return new Response("Customer is not subscribed", { status: 403 }); + } + + const isOverLimit = AIUsage.isOverLimit(customerId); + if (isOverLimit) { + return new Response("Monthly usage limit exceeded", { status: 429 }); + } + + // Generate the translation using AI + const { text, usage } = await generateText({ + model: aiClient(MODEL_ID), + maxOutputTokens: 2_000, + messages: [ + { + role: "system", + content: systemPrompt, + }, + { + role: "user", + content: makeUserPrompt(sourceLanguage, targetLanguage, comment), + }, + ], + }); + + await AIUsage.trackUsage(customerId, MODEL_ID, usage); + + return new Response(text, { + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/routes/ai/translatePost.ts b/routes/ai/translatePost.ts new file mode 100644 index 0000000..7af8f50 --- /dev/null +++ b/routes/ai/translatePost.ts @@ -0,0 +1,100 @@ +import { z } from "zod"; +import { verifySubscription } from "../../middleware/subscription"; +import { aiClient } from "../../utils/models"; +import { ai_provider } from "../../utils/models"; +import { generateText } from "ai"; +import { AIUsage } from "../../services/AIUsage"; + +const translatePostSchema = z.object({ + customerId: z.string(), + postTitle: z.string(), + postText: z.string(), + sourceLanguage: z.string(), + targetLanguage: z.string(), +}); + +const systemPrompt = `You are a helpful assistant that translates text. Translate the given text accurately while preserving the original meaning, tone, and context. Only provide the translation without any additional explanations or formatting.`; + +const makeUserPrompt = ( + sourceLanguage: string, + targetLanguage: string, + postTitle: string, + postText: string, +) => { + const sourceLangName = sourceLanguage === "auto" ? "automatically detected language" : + sourceLanguage === "en" ? "English" : + sourceLanguage === "zh-Hant" ? "Chinese Traditional" : + sourceLanguage === "zh-Hans" ? "Chinese Simplified" : + sourceLanguage === "es" ? "Spanish" : + sourceLanguage === "fr" ? "French" : + sourceLanguage === "de" ? "German" : + sourceLanguage === "pt" ? "Portuguese" : + sourceLanguage === "ja" ? "Japanese" : + sourceLanguage === "ko" ? "Korean" : + sourceLanguage === "ru" ? "Russian" : sourceLanguage; + + const targetLangName = + targetLanguage === "en" ? "English" : + targetLanguage === "zh-Hant" ? "Chinese Traditional" : + targetLanguage === "zh-Hans" ? "Chinese Simplified" : + targetLanguage === "es" ? "Spanish" : + targetLanguage === "fr" ? "French" : + targetLanguage === "de" ? "German" : + targetLanguage === "pt" ? "Portuguese" : + targetLanguage === "ja" ? "Japanese" : + targetLanguage === "ko" ? "Korean" : + targetLanguage === "ru" ? "Russian" : targetLanguage; + + return ` +Translate the following Reddit post from ${sourceLangName} to ${targetLangName}: + +Title: ${postTitle} + +${postText ? `Post Content:\n${postText}` : ""} +`; +}; + +let MODEL_ID: string = ""; + +if (ai_provider == "groq") { + MODEL_ID = "openai/gpt-oss-20b"; +} else { + MODEL_ID = process.env.OPENAI_SUMMARY_MODEL || "gpt-4.1-mini"; +} + +export async function translatePost(req: Request) { + const body = await req.json(); + const { customerId, postTitle, postText, sourceLanguage, targetLanguage } = + translatePostSchema.parse(body); + const isSubscribed = await verifySubscription(customerId); + if (!isSubscribed) { + return new Response("Customer is not subscribed", { status: 403 }); + } + + const isOverLimit = AIUsage.isOverLimit(customerId); + if (isOverLimit) { + return new Response("Monthly usage limit exceeded", { status: 429 }); + } + + // Generate the translation using AI + const { text, usage } = await generateText({ + model: aiClient(MODEL_ID), + maxOutputTokens: 2_000, + messages: [ + { + role: "system", + content: systemPrompt, + }, + { + role: "user", + content: makeUserPrompt(sourceLanguage, targetLanguage, postTitle, postText), + }, + ], + }); + + await AIUsage.trackUsage(customerId, MODEL_ID, usage); + + return new Response(text, { + headers: { "Content-Type": "application/json" }, + }); +} From f4cceb51694879d1a5d3d0c9631d3d327ca17166 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Tue, 24 Mar 2026 07:21:05 -0700 Subject: [PATCH 3/3] Consolidate duplicate translatePost/translateComment routes into single translate endpoint DRY: merge two nearly-identical route handlers (shared system prompt, language-name mapping, model selection, subscription/spend checks) into one /api/ai/translate route accepting type: "post" | "comment". Co-Authored-By: Claude Opus 4.6 --- routes/ai/index.ts | 10 +- .../ai/{translateComment.ts => translate.ts} | 81 +++++++------- routes/ai/translatePost.ts | 100 ------------------ 3 files changed, 47 insertions(+), 144 deletions(-) rename routes/ai/{translateComment.ts => translate.ts} (53%) delete mode 100644 routes/ai/translatePost.ts diff --git a/routes/ai/index.ts b/routes/ai/index.ts index fc4ede1..c48fdc4 100644 --- a/routes/ai/index.ts +++ b/routes/ai/index.ts @@ -3,8 +3,7 @@ import { summarizeComments } from "./summarizeComments"; import { filterPosts } from "./filterPosts"; import { createEmbedding } from "./createEmbedding"; import { askQuestion } from "./askQuestion"; -import { translatePost } from "./translatePost"; -import { translateComment } from "./translateComment"; +import { translate } from "./translate"; export default { "/api/ai/summarizePostDetails": { @@ -13,11 +12,8 @@ export default { "/api/ai/summarizeComments": { POST: summarizeComments, }, - "/api/ai/translatePost": { - POST: translatePost, - }, - "/api/ai/translateComment": { - POST: translateComment, + "/api/ai/translate": { + POST: translate, }, "/api/ai/filterPosts": { POST: filterPosts, diff --git a/routes/ai/translateComment.ts b/routes/ai/translate.ts similarity index 53% rename from routes/ai/translateComment.ts rename to routes/ai/translate.ts index 46eae14..65b2ab7 100644 --- a/routes/ai/translateComment.ts +++ b/routes/ai/translate.ts @@ -5,49 +5,56 @@ import { ai_provider } from "../../utils/models"; import { generateText } from "ai"; import { AIUsage } from "../../services/AIUsage"; -const translateCommentSchema = z.object({ +const translateSchema = z.object({ customerId: z.string(), - comment: z.string(), + type: z.enum(["post", "comment"]), sourceLanguage: z.string(), targetLanguage: z.string(), + // Post-specific fields + postTitle: z.string().optional(), + postText: z.string().optional(), + // Comment-specific field + comment: z.string().optional(), }); const systemPrompt = `You are a helpful assistant that translates text. Translate the given text accurately while preserving the original meaning, tone, and context. Only provide the translation without any additional explanations or formatting.`; +const LANGUAGE_NAMES: Record = { + auto: "automatically detected language", + en: "English", + "zh-Hant": "Chinese Traditional", + "zh-Hans": "Chinese Simplified", + es: "Spanish", + fr: "French", + de: "German", + pt: "Portuguese", + ja: "Japanese", + ko: "Korean", + ru: "Russian", +}; + +const getLangName = (code: string) => LANGUAGE_NAMES[code] ?? code; + const makeUserPrompt = ( sourceLanguage: string, targetLanguage: string, - comment: string, + type: "post" | "comment", + postTitle?: string, + postText?: string, + comment?: string, ) => { - const sourceLangName = sourceLanguage === "auto" ? "automatically detected language" : - sourceLanguage === "en" ? "English" : - sourceLanguage === "zh-Hant" ? "Chinese Traditional" : - sourceLanguage === "zh-Hans" ? "Chinese Simplified" : - sourceLanguage === "es" ? "Spanish" : - sourceLanguage === "fr" ? "French" : - sourceLanguage === "de" ? "German" : - sourceLanguage === "pt" ? "Portuguese" : - sourceLanguage === "ja" ? "Japanese" : - sourceLanguage === "ko" ? "Korean" : - sourceLanguage === "ru" ? "Russian" : sourceLanguage; - - const targetLangName = - targetLanguage === "en" ? "English" : - targetLanguage === "zh-Hant" ? "Chinese Traditional" : - targetLanguage === "zh-Hans" ? "Chinese Simplified" : - targetLanguage === "es" ? "Spanish" : - targetLanguage === "fr" ? "French" : - targetLanguage === "de" ? "German" : - targetLanguage === "pt" ? "Portuguese" : - targetLanguage === "ja" ? "Japanese" : - targetLanguage === "ko" ? "Korean" : - targetLanguage === "ru" ? "Russian" : targetLanguage; - - return ` -Translate the following comment from ${sourceLangName} to ${targetLangName}: - -${comment} -`; + const sourceLangName = getLangName(sourceLanguage); + const targetLangName = getLangName(targetLanguage); + + if (type === "comment") { + return `Translate the following comment from ${sourceLangName} to ${targetLangName}:\n\n${comment}`; + } + + return `Translate the following Reddit post from ${sourceLangName} to ${targetLangName}: + +Title: ${postTitle} + +${postText ? `Post Content:\n${postText}` : ""}`; }; let MODEL_ID: string = ""; @@ -58,10 +65,11 @@ if (ai_provider == "groq") { MODEL_ID = process.env.OPENAI_SUMMARY_MODEL || "gpt-4.1-mini"; } -export async function translateComment(req: Request) { +export async function translate(req: Request) { const body = await req.json(); - const { customerId, comment, sourceLanguage, targetLanguage } = - translateCommentSchema.parse(body); + const { customerId, type, sourceLanguage, targetLanguage, postTitle, postText, comment } = + translateSchema.parse(body); + const isSubscribed = await verifySubscription(customerId); if (!isSubscribed) { return new Response("Customer is not subscribed", { status: 403 }); @@ -72,7 +80,6 @@ export async function translateComment(req: Request) { return new Response("Monthly usage limit exceeded", { status: 429 }); } - // Generate the translation using AI const { text, usage } = await generateText({ model: aiClient(MODEL_ID), maxOutputTokens: 2_000, @@ -83,7 +90,7 @@ export async function translateComment(req: Request) { }, { role: "user", - content: makeUserPrompt(sourceLanguage, targetLanguage, comment), + content: makeUserPrompt(sourceLanguage, targetLanguage, type, postTitle, postText, comment), }, ], }); diff --git a/routes/ai/translatePost.ts b/routes/ai/translatePost.ts deleted file mode 100644 index 7af8f50..0000000 --- a/routes/ai/translatePost.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from "zod"; -import { verifySubscription } from "../../middleware/subscription"; -import { aiClient } from "../../utils/models"; -import { ai_provider } from "../../utils/models"; -import { generateText } from "ai"; -import { AIUsage } from "../../services/AIUsage"; - -const translatePostSchema = z.object({ - customerId: z.string(), - postTitle: z.string(), - postText: z.string(), - sourceLanguage: z.string(), - targetLanguage: z.string(), -}); - -const systemPrompt = `You are a helpful assistant that translates text. Translate the given text accurately while preserving the original meaning, tone, and context. Only provide the translation without any additional explanations or formatting.`; - -const makeUserPrompt = ( - sourceLanguage: string, - targetLanguage: string, - postTitle: string, - postText: string, -) => { - const sourceLangName = sourceLanguage === "auto" ? "automatically detected language" : - sourceLanguage === "en" ? "English" : - sourceLanguage === "zh-Hant" ? "Chinese Traditional" : - sourceLanguage === "zh-Hans" ? "Chinese Simplified" : - sourceLanguage === "es" ? "Spanish" : - sourceLanguage === "fr" ? "French" : - sourceLanguage === "de" ? "German" : - sourceLanguage === "pt" ? "Portuguese" : - sourceLanguage === "ja" ? "Japanese" : - sourceLanguage === "ko" ? "Korean" : - sourceLanguage === "ru" ? "Russian" : sourceLanguage; - - const targetLangName = - targetLanguage === "en" ? "English" : - targetLanguage === "zh-Hant" ? "Chinese Traditional" : - targetLanguage === "zh-Hans" ? "Chinese Simplified" : - targetLanguage === "es" ? "Spanish" : - targetLanguage === "fr" ? "French" : - targetLanguage === "de" ? "German" : - targetLanguage === "pt" ? "Portuguese" : - targetLanguage === "ja" ? "Japanese" : - targetLanguage === "ko" ? "Korean" : - targetLanguage === "ru" ? "Russian" : targetLanguage; - - return ` -Translate the following Reddit post from ${sourceLangName} to ${targetLangName}: - -Title: ${postTitle} - -${postText ? `Post Content:\n${postText}` : ""} -`; -}; - -let MODEL_ID: string = ""; - -if (ai_provider == "groq") { - MODEL_ID = "openai/gpt-oss-20b"; -} else { - MODEL_ID = process.env.OPENAI_SUMMARY_MODEL || "gpt-4.1-mini"; -} - -export async function translatePost(req: Request) { - const body = await req.json(); - const { customerId, postTitle, postText, sourceLanguage, targetLanguage } = - translatePostSchema.parse(body); - const isSubscribed = await verifySubscription(customerId); - if (!isSubscribed) { - return new Response("Customer is not subscribed", { status: 403 }); - } - - const isOverLimit = AIUsage.isOverLimit(customerId); - if (isOverLimit) { - return new Response("Monthly usage limit exceeded", { status: 429 }); - } - - // Generate the translation using AI - const { text, usage } = await generateText({ - model: aiClient(MODEL_ID), - maxOutputTokens: 2_000, - messages: [ - { - role: "system", - content: systemPrompt, - }, - { - role: "user", - content: makeUserPrompt(sourceLanguage, targetLanguage, postTitle, postText), - }, - ], - }); - - await AIUsage.trackUsage(customerId, MODEL_ID, usage); - - return new Response(text, { - headers: { "Content-Type": "application/json" }, - }); -}