|
| 1 | +import type { VercelRequest, VercelResponse } from "@vercel/node"; |
| 2 | + |
| 3 | +const VALID_LANG_CODES = new Set([ |
| 4 | + "en", "de", "fr", "es", "zh-CN", "zh-TW", "ru", "it", "pt", "ro", |
| 5 | + "ja", "ko", "vi", "id", "bg", "cs", |
| 6 | + "EN", "DE", "FR", "ES", "ZH", "ZH-CN", "ZH-TW", "RU", "IT", "PT", "RO", |
| 7 | + "JA", "KO", "VI", "ID", "BG", "CS", |
| 8 | + "EN-GB", "PT-PT", "ZH-HANS", "ZH-HANT", |
| 9 | +]); |
| 10 | + |
| 11 | +const MAX_TEXT_LENGTH = 200; |
| 12 | +const rateLimitMap = new Map<string, { count: number; resetAt: number }>(); |
| 13 | + |
| 14 | +function checkRateLimit(ip: string): boolean { |
| 15 | + const now = Date.now(); |
| 16 | + const entry = rateLimitMap.get(ip); |
| 17 | + if (!entry || now > entry.resetAt) { |
| 18 | + rateLimitMap.set(ip, { count: 1, resetAt: now + 60_000 }); |
| 19 | + return true; |
| 20 | + } |
| 21 | + if (entry.count >= 10) return false; |
| 22 | + entry.count++; |
| 23 | + return true; |
| 24 | +} |
| 25 | + |
| 26 | +export default async function handler(req: VercelRequest, res: VercelResponse) { |
| 27 | + res.setHeader("Access-Control-Allow-Origin", "*"); |
| 28 | + res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); |
| 29 | + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |
| 30 | + |
| 31 | + if (req.method === "OPTIONS") { |
| 32 | + res.status(200).end(); |
| 33 | + return; |
| 34 | + } |
| 35 | + |
| 36 | + if (req.method !== "POST") { |
| 37 | + res.status(405).json({ error: "Method not allowed" }); |
| 38 | + return; |
| 39 | + } |
| 40 | + |
| 41 | + const ip = (req.headers["x-forwarded-for"] as string || "unknown").split(",")[0].trim(); |
| 42 | + if (!checkRateLimit(ip)) { |
| 43 | + res.status(429).json({ error: "Rate limit exceeded. Please try again later." }); |
| 44 | + return; |
| 45 | + } |
| 46 | + |
| 47 | + const { text, source, target } = req.body || {}; |
| 48 | + |
| 49 | + if (typeof text !== "string" || !text.trim()) { |
| 50 | + res.status(400).json({ error: "Missing or empty 'text' field" }); |
| 51 | + return; |
| 52 | + } |
| 53 | + if (text.length > MAX_TEXT_LENGTH) { |
| 54 | + res.status(400).json({ error: `Text exceeds maximum length of ${MAX_TEXT_LENGTH} characters` }); |
| 55 | + return; |
| 56 | + } |
| 57 | + if (typeof source !== "string" || !VALID_LANG_CODES.has(source)) { |
| 58 | + res.status(400).json({ error: `Invalid 'source' language code: ${source}` }); |
| 59 | + return; |
| 60 | + } |
| 61 | + if (typeof target !== "string" || !VALID_LANG_CODES.has(target)) { |
| 62 | + res.status(400).json({ error: `Invalid 'target' language code: ${target}` }); |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + const mlServer = process.env["ML_SERVER"]; |
| 67 | + const apiKey = process.env["ML_API_KEY"]; |
| 68 | + if (!mlServer || !apiKey) { |
| 69 | + res.status(500).json({ error: "Translation service is not configured" }); |
| 70 | + return; |
| 71 | + } |
| 72 | + |
| 73 | + try { |
| 74 | + const params = new URLSearchParams({ |
| 75 | + API_KEY: apiKey, |
| 76 | + langTo: target.toLowerCase(), |
| 77 | + langFrom: source.toLowerCase(), |
| 78 | + }); |
| 79 | + |
| 80 | + const response = await fetch(`${mlServer}/translate?${params.toString()}`, { |
| 81 | + method: "POST", |
| 82 | + headers: { "Content-Type": "application/json; charset=utf-8" }, |
| 83 | + body: JSON.stringify({ "X-Translate-Text": text.trim() }), |
| 84 | + }); |
| 85 | + |
| 86 | + if (!response.ok) { |
| 87 | + const errorBody = await response.text(); |
| 88 | + console.error("ML API error", response.status, errorBody); |
| 89 | + res.status(502).json({ error: "Translation service returned an error" }); |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + const data: any = await response.json(); |
| 94 | + const translated = data?.[1]?.data?.translations?.[0]?.translatedText; |
| 95 | + |
| 96 | + if (!translated) { |
| 97 | + console.error("Unexpected ML response format", JSON.stringify(data)); |
| 98 | + res.status(502).json({ error: "Unexpected response from translation service" }); |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + res.status(200).json({ translation: translated }); |
| 103 | + } catch (err) { |
| 104 | + console.error("ML request failed", err); |
| 105 | + res.status(502).json({ error: "Failed to reach translation service" }); |
| 106 | + } |
| 107 | +} |
0 commit comments