/** * The Emoji Language — Translator API (Cloudflare Worker) * ======================================================= * This is the tiny free backend for emoji-translator.html. It keeps your * Anthropic API key secret (a key pasted into Squarespace HTML could be * stolen by anyone who views the page source), and it re-downloads your * published grammar Google Doc + dictionary sheet every 15 minutes — so * edits to the master document flow into translations automatically. * * SETUP (one time, ~5 minutes): * 1. Create a free account at https://dash.cloudflare.com * 2. Workers & Pages → Create → "Start with Hello World" worker. * Name it e.g. "emoji-translator" → Deploy. * 3. Click "Edit code", replace everything with THIS file → Deploy. * 4. Back on the worker page: Settings → Variables and Secrets → * Add → Type: "Secret" → Name: ANTHROPIC_API_KEY → * Value: your key from https://console.anthropic.com → Save. * 5. Copy your worker URL (looks like * https://emoji-translator.YOURNAME.workers.dev) and paste it into * the WORKER_URL constant at the top of emoji-translator.html. * * Optional hardening: once it works, put your site's address in * ALLOWED_ORIGINS below so only your Squarespace site can use it. */ // The Claude model that does the translating. // Most accurate: "claude-opus-4-8". Cheaper/faster alternatives you can // swap in by editing this one line: "claude-sonnet-5" or "claude-haiku-4-5". const MODEL = "claude-opus-4-8"; // Your live sources (fetched fresh every SOURCE_TTL_MS): const GRAMMAR_DOC_URL = "https://docs.google.com/document/d/e/2PACX-1vTd_xvkazuZgDD9tWc01qWuCLLFDTS5ro3dMDQgh-O3ZLRi23UBc4o49z5e3PQD4o_rFeK_s9fstRIV/pub"; const DICTIONARY_CSV_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRlmweRgDgIyrVsLf3JxDkqENmuX3k01B5uqCnutA8LmIAHdnkA02vXz59ik7mJSo7cf2x4swg4Ox5L/pub?output=csv"; const SOURCE_TTL_MS = 15 * 60 * 1000; // re-fetch grammar + dictionary every 15 min const MAX_INPUT_CHARS = 1500; // per-request input cap const RATE_LIMIT_PER_MIN = 12; // per-visitor requests per minute // Leave empty to allow any site (fine while testing). To lock it down, list // your domains, e.g. ["https://www.theemojilanguage.com", "https://theemojilanguage.com"] const ALLOWED_ORIGINS = []; /* ------------------------------------------------------------------ */ // Cached copies of the grammar + dictionary (kept warm between requests). let sourceCache = { grammar: null, dictionary: null, fetchedAt: 0 }; // Very light per-IP rate limiting. const rateBuckets = new Map(); const STABLE_INSTRUCTIONS = `You are the official translator for "The Emoji Language", a constructed written language created by Ryan Zin. The language's own name is the two-emoji word 🗣️😁 — always use exactly 🗣️😁 whenever the text refers to The Emoji Language itself. You translate the way an expert human translator does: MEANING-FIRST. Understand what the source text means — including idioms, implied subjects, and tone — then re-express that meaning natively in the target language, completely reworking the grammar to fit it. Never translate word-for-word. The complete grammar of The Emoji Language and its established dictionary follow in the next sections. The grammar is authoritative: follow it strictly. WHEN TRANSLATING INTO THE EMOJI LANGUAGE: - Every word is exactly 2 emoji; write with NO SPACES between words (numbers and Latin-script proper nouns are the only exceptions, per the grammar). - Strict Subject-Verb-Object order. Every verb is immediately preceded by a tense marker. Modifiers (adjectives, adverbs, possessors, demonstratives, numbers) come AFTER the word they modify. Objects and post-verb nouns are introduced by the correct preposition (direct and indirect objects by ➡️➡️). Negation ❌❌ goes immediately before the tense marker. - End sentences with the correct punctuation: ⚫️ separates sentences; ❗️❗️ after emphasized/imperative sentences and ❓❓ after questions are MANDATORY. - Prefer the established dictionary vocabulary. If a concept has no dictionary entry, coin a new 2-emoji word using the derivation rules (the second emoji describes the first), choose the most universally recognizable emoji, and stay consistent within the translation. - Restructure freely: convert idioms to their meaning first, drop articles, drop the verb "to be", use the it/there constructions, and pick the tense that matches the source meaning (e.g. English present progressive and simple present both map to the present tense). WHEN TRANSLATING FROM THE EMOJI LANGUAGE: - Segment the text into 2-emoji words, use the grammar to recover the sentence structure, then write natural, fluent prose in the requested target language — not a robotic word-by-word gloss. Choose the most natural phrasing a native speaker would use. - If a word is not in the dictionary, infer its meaning from the two emoji using the derivation rules. OUTPUT RULES: - Reply with ONLY the translation. No explanations, no quotes, no labels, no commentary. - If the input is empty or cannot be interpreted at all, reply with exactly: ⛔️`; export default { async fetch(request, env) { const cors = corsHeaders(request); if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: cors }); } if (request.method !== "POST") { return json({ ok: true, service: "The Emoji Language translator API" }, 200, cors); } if (ALLOWED_ORIGINS.length && !ALLOWED_ORIGINS.includes(request.headers.get("Origin"))) { return json({ ok: false, error: "Origin not allowed." }, 403, cors); } if (!env.ANTHROPIC_API_KEY) { return json({ ok: false, error: "Server not configured: missing ANTHROPIC_API_KEY secret." }, 500, cors); } if (!checkRateLimit(request)) { return json({ ok: false, error: "Too many requests — please wait a moment." }, 429, cors); } let body; try { body = await request.json(); } catch { body = null; } const text = (body && typeof body.text === "string") ? body.text.trim() : ""; const direction = (body && body.direction === "toText") ? "toText" : "toEmoji"; const lang = (body && typeof body.lang === "string" && body.lang.length <= 40) ? body.lang.replace(/[^\p{L}\p{N} ()\-]/gu, "") : "English"; if (!text) return json({ ok: false, error: "Nothing to translate." }, 400, cors); if (text.length > MAX_INPUT_CHARS) { return json({ ok: false, error: `Text is too long (max ${MAX_INPUT_CHARS} characters).` }, 400, cors); } try { const { grammar, dictionary } = await getSources(); const userPrompt = direction === "toEmoji" ? `Translate the following text into The Emoji Language:\n\n${text}` : `Translate the following Emoji Language text into natural, fluent ${lang}:\n\n${text}`; const apiResp = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", }, body: JSON.stringify({ model: MODEL, max_tokens: 16000, thinking: { type: "adaptive" }, system: [ { type: "text", text: STABLE_INSTRUCTIONS }, { type: "text", text: "=== THE EMOJI LANGUAGE — COMPLETE GRAMMAR (live master document) ===\n\n" + grammar }, { type: "text", text: "=== THE EMOJI LANGUAGE — DICTIONARY (CSV: emoji word, meanings) ===\n\n" + dictionary, // Caches the whole system prompt (instructions + grammar + dictionary) // so repeat translations within ~5 minutes cost ~10% as much. cache_control: { type: "ephemeral" }, }, ], messages: [{ role: "user", content: userPrompt }], }), }); const data = await apiResp.json(); if (!apiResp.ok) { const msg = (data && data.error && data.error.message) ? data.error.message : `API error (${apiResp.status})`; return json({ ok: false, error: msg }, 502, cors); } if (data.stop_reason === "refusal") { return json({ ok: false, error: "The translator declined this request." }, 200, cors); } const translation = (data.content || []) .filter((b) => b.type === "text") .map((b) => b.text) .join("") .trim(); if (!translation) return json({ ok: false, error: "No translation was produced — please try again." }, 200, cors); return json({ ok: true, translation }, 200, cors); } catch (err) { return json({ ok: false, error: "Translation failed: " + (err && err.message ? err.message : "unknown error") }, 500, cors); } }, }; /* ---------------- live grammar + dictionary fetching ---------------- */ async function getSources() { const now = Date.now(); if (sourceCache.grammar && now - sourceCache.fetchedAt < SOURCE_TTL_MS) return sourceCache; const [gRes, dRes] = await Promise.all([ fetch(GRAMMAR_DOC_URL, { headers: { "User-Agent": "Mozilla/5.0" } }), fetch(DICTIONARY_CSV_URL, { headers: { "User-Agent": "Mozilla/5.0" } }), ]); if (!gRes.ok && !sourceCache.grammar) throw new Error("could not load the grammar document"); if (!dRes.ok && !sourceCache.dictionary) throw new Error("could not load the dictionary"); const grammar = gRes.ok ? htmlToText(await gRes.text()) : sourceCache.grammar; const dictionary = dRes.ok ? (await dRes.text()).trim() : sourceCache.dictionary; sourceCache = { grammar, dictionary, fetchedAt: now }; return sourceCache; } // Strips a published-Google-Doc page down to readable plain text. function htmlToText(html) { let t = html .replace(//gi, " ") .replace(//gi, " ") .replace(/<(?:br|\/p|\/div|\/tr|\/h[1-6]|\/li)[^>]*>/gi, "\n") .replace(/<\/td>/gi, "\t") .replace(/<[^>]+>/g, "") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10))) .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16))); return t.replace(/[ \t]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n").trim(); } /* ------------------------- small helpers --------------------------- */ function corsHeaders(request) { const origin = request.headers.get("Origin") || "*"; const allow = ALLOWED_ORIGINS.length ? (ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]) : "*"; return { "Access-Control-Allow-Origin": allow, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "86400", }; } function checkRateLimit(request) { const ip = request.headers.get("CF-Connecting-IP") || "unknown"; const now = Date.now(); let bucket = rateBuckets.get(ip); if (!bucket || now - bucket.start > 60000) { bucket = { start: now, count: 0 }; rateBuckets.set(ip, bucket); } bucket.count++; if (rateBuckets.size > 5000) rateBuckets.clear(); // keep memory bounded return bucket.count <= RATE_LIMIT_PER_MIN; } function json(obj, status, cors) { return new Response(JSON.stringify(obj), { status, headers: { "Content-Type": "application/json", ...cors }, }); }