export default { async fetch(request, env, ctx) { const url = new URL(request.url); const { pathname } = url; // CORS preflight for API if (request.method === "OPTIONS" && pathname.startsWith("/api/")) { return new Response(null, { headers: corsHeaders() }); } // --- API: list languages (common) --- if (pathname === "/api/languages") { return jsonResponse({ languages: COMMON_LANGUAGES }, 200, corsHeaders()); } // --- API: translate --- if (pathname === "/api/translate") { if (request.method !== "POST") { return jsonResponse( { error: "Use POST /api/translate with JSON body." }, 405, corsHeaders() ); } // Optional API key const apiKey = env.API_KEY; if (apiKey) { const got = request.headers.get("x-api-key") || url.searchParams.get("key") || ""; if (got !== apiKey) { return jsonResponse({ error: "Unauthorized" }, 401, corsHeaders()); } } let body; try { body = await request.json(); } catch { return jsonResponse({ error: "Invalid JSON body." }, 400, corsHeaders()); } const text = (body.text ?? "").toString(); const target_lang = (body.target_lang ?? "").toString().trim(); const source_lang_raw = (body.source_lang ?? "").toString().trim(); // optional if (!text) return jsonResponse({ error: "text is required" }, 400, corsHeaders()); if (!target_lang) return jsonResponse({ error: "target_lang is required" }, 400, corsHeaders()); const page_size = clampInt(body.page_size ?? 1800, 200, 5000); const page = clampInt(body.page ?? 1, 1, 999999); const translate_all = Boolean(body.translate_all); const chunks = splitText(text, page_size); const total_pages = chunks.length; const safePage = clampInt(page, 1, total_pages); try { if (translate_all) { // Use batch requests (Workers AI supports batch schema on this model) :contentReference[oaicite:2]{index=2} const requests = chunks.slice(0, 50).map((t) => ({ text: t, ...(source_lang_raw ? { source_lang: normalizeLang(source_lang_raw) } : {}), target_lang: normalizeLang(target_lang), })); const out = await env.AI.run("@cf/meta/m2m100-1.2b", { requests }); // Some bindings return array-like results; normalize defensively: const translated_pages = Array.isArray(out) ? out.map((x) => x?.translated_text ?? "") : (out?.responses ?? out?.results ?? out?.translated_pages ?? null); // Fallback: if unknown, just translate one-by-one if (Array.isArray(translated_pages)) { return jsonResponse( { page: 1, page_size, total_pages, translated_pages, translated_text: translated_pages.join(""), truncated: chunks.length > 50, }, 200, corsHeaders() ); } } // Default: translate one page const chunk = chunks[safePage - 1] ?? ""; const out = await env.AI.run("@cf/meta/m2m100-1.2b", { text: chunk, ...(source_lang_raw ? { source_lang: normalizeLang(source_lang_raw) } : {}), target_lang: normalizeLang(target_lang), }); return jsonResponse( { page: safePage, page_size, total_pages, source_lang: source_lang_raw || "(default en)", target_lang, input_text: chunk, translated_text: out?.translated_text ?? "", }, 200, corsHeaders() ); } catch (e) { return jsonResponse( { error: "AI run failed", detail: String(e?.message || e) }, 500, corsHeaders() ); } } // --- Web UI --- if (pathname === "/" || pathname === "/index.html") { if (request.method === "GET") { return new Response(renderPage({}), { headers: { "content-type": "text/html; charset=utf-8" }, }); } if (request.method === "POST") { const form = await request.formData(); const text = (form.get("text") ?? "").toString(); const source_lang = (form.get("source_lang") ?? "en").toString().trim(); const target_lang = (form.get("target_lang") ?? "zh").toString().trim(); const page_size = clampInt(form.get("page_size") ?? 1800, 200, 5000); const page = clampInt(form.get("page") ?? 1, 1, 999999); if (!text) { return new Response(renderPage({ error: "请输入要翻译的文本。", text, source_lang, target_lang, page_size, page }), { headers: { "content-type": "text/html; charset=utf-8" }, }); } const chunks = splitText(text, page_size); const total_pages = chunks.length; const safePage = clampInt(page, 1, total_pages); const chunk = chunks[safePage - 1] ?? ""; let translated = ""; let err = ""; try { const out = await env.AI.run("@cf/meta/m2m100-1.2b", { text: chunk, source_lang: normalizeLang(source_lang), target_lang: normalizeLang(target_lang), }); translated = out?.translated_text ?? ""; } catch (e) { err = `翻译失败:${String(e?.message || e)}`; } return new Response( renderPage({ text, source_lang, target_lang, page_size, page: safePage, total_pages, input_chunk: chunk, translated_text: translated, error: err, }), { headers: { "content-type": "text/html; charset=utf-8" } } ); } return new Response("Method Not Allowed", { status: 405 }); } return new Response("Not Found", { status: 404 }); }, }; // ---------------- helpers ---------------- const COMMON_LANGUAGES = [ { code: "en", name: "English" }, { code: "zh", name: "中文" }, { code: "ja", name: "日本語" }, { code: "ko", name: "한국어" }, { code: "fr", name: "Français" }, { code: "de", name: "Deutsch" }, { code: "es", name: "Español" }, { code: "pt", name: "Português" }, { code: "it", name: "Italiano" }, { code: "ru", name: "Русский" }, { code: "ar", name: "العربية" }, { code: "hi", name: "हिन्दी" }, { code: "id", name: "Bahasa Indonesia" }, { code: "th", name: "ไทย" }, { code: "vi", name: "Tiếng Việt" }, { code: "tr", name: "Türkçe" }, { code: "nl", name: "Nederlands" }, { code: "sv", name: "Svenska" }, { code: "pl", name: "Polski" }, { code: "uk", name: "Українська" }, ]; function normalizeLang(lang) { // Docs say language code like 'en'/'es' etc. :contentReference[oaicite:3]{index=3} // But examples sometimes use english/french; we accept both by passing through. return String(lang || "").trim().toLowerCase(); } function clampInt(v, min, max) { const n = Number.parseInt(String(v), 10); if (!Number.isFinite(n)) return min; return Math.min(max, Math.max(min, n)); } function splitText(text, maxChars) { const t = String(text || ""); if (t.length <= maxChars) return [t]; // Prefer paragraph-based splitting const parts = t.split(/\n{2,}/); const chunks = []; let buf = ""; for (const p of parts) { const piece = (p + "\n\n"); if (piece.length > maxChars) { // Hard split long paragraph if (buf) { chunks.push(buf); buf = ""; } for (let i = 0; i < piece.length; i += maxChars) { chunks.push(piece.slice(i, i + maxChars)); } continue; } if (buf.length + piece.length > maxChars) { chunks.push(buf); buf = piece; } else { buf += piece; } } if (buf) chunks.push(buf); return chunks; } function escapeHtml(s) { return String(s || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function renderPage(state) { const { text = "", source_lang = "en", target_lang = "zh", page_size = 1800, page = 1, total_pages = 1, input_chunk = "", translated_text = "", error = "", } = state || {}; const langOptions = (selected) => COMMON_LANGUAGES.map( (l) => `` ).join(""); const hasResult = Boolean(text); return `
${escapeHtml(error)}
` : ""} ${ hasResult ? `${escapeHtml(input_chunk || "")}
${escapeHtml(translated_text || "")}