/**
* 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(/