Last active
January 21, 2026 18:10
-
-
Save misterburton/3b4f99e1996874884833833ddd45d527 to your computer and use it in GitHub Desktop.
Vercel API endpoint for AI translation using Claude Opus 4.5 + Vercel KV caching
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * translate-api.js (Vercel Serverless Function) | |
| * | |
| * A Vercel API endpoint that translates content using Anthropic's Claude Opus 4.5 | |
| * and caches results in Vercel KV for fast retrieval. | |
| * | |
| * REQUIREMENTS: | |
| * - Vercel project with KV database configured | |
| * - npm packages: @anthropic-ai/sdk, @vercel/kv | |
| * - Environment variables in Vercel dashboard or .env.local: | |
| * - ANTHROPIC_API_KEY: Your Anthropic API key | |
| * - KV_REST_API_URL: Auto-set when you create a Vercel KV store | |
| * - KV_REST_API_TOKEN: Auto-set when you create a Vercel KV store | |
| * | |
| * SETUP: | |
| * 1. Create a Vercel KV store in your Vercel dashboard | |
| * 2. Add ANTHROPIC_API_KEY to your Vercel environment variables | |
| * 3. Run `vercel env pull .env.local` to sync credentials locally | |
| * 4. Place this file at /api/translate.js in your Vercel project | |
| * | |
| * API CONTRACT: | |
| * POST /api/translate | |
| * Body: { | |
| * pageId: string, // Page identifier (e.g., "home", "about") | |
| * content: object, // Key-value pairs of l10n-id to source text | |
| * targetLanguage: string, // Full language name (e.g., "Spanish", "Chinese") | |
| * contentHash: string, // SHA-256 hash of normalized source content | |
| * bypassCache?: boolean // Force fresh translation (used by pre-translate.js) | |
| * } | |
| * | |
| * Response: { | |
| * translatedContent: object, // Key-value pairs of l10n-id to translated text | |
| * cached: boolean // Whether result came from cache | |
| * } | |
| * | |
| * CACHING STRATEGY: | |
| * - Cache key format: `trans:{pageId}:{targetLanguage}` | |
| * - pre-translate.js uses bypassCache=true to force fresh translations | |
| * - Client requests use cache if available (no hash validation needed) | |
| * - Hash is stored with cache entry for debugging/verification | |
| * | |
| * @license MIT | |
| */ | |
| const Anthropic = require("@anthropic-ai/sdk").default; | |
| const { createClient } = require("@vercel/kv"); | |
| // Initialize KV client only if environment variables are present | |
| let kv = null; | |
| if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { | |
| kv = createClient({ | |
| url: process.env.KV_REST_API_URL, | |
| token: process.env.KV_REST_API_TOKEN, | |
| }); | |
| } else { | |
| console.error('Vercel KV environment variables are missing. Caching will be disabled.'); | |
| console.error('Ensure KV_REST_API_URL and KV_REST_API_TOKEN are set in your .env.local file.'); | |
| console.error('Run: vercel env pull .env.local to sync them from the Vercel dashboard.'); | |
| } | |
| module.exports = async (req, res) => { | |
| // Only accept POST requests | |
| if (req.method !== 'POST') { | |
| return res.status(405).json({ error: 'Method not allowed' }); | |
| } | |
| const { pageId, content, targetLanguage, contentHash, bypassCache } = req.body; | |
| // Validate required parameters | |
| if (!pageId || !content || !targetLanguage || !contentHash) { | |
| return res.status(400).json({ error: 'Missing required parameters' }); | |
| } | |
| const cacheKey = `trans:${pageId}:${targetLanguage}`; | |
| // ======================================================================== | |
| // STEP 1: Check Vercel KV Cache | |
| // ======================================================================== | |
| // NOTE: We skip hash validation for browser-to-API requests. | |
| // Hash mismatches between JSDOM (pre-translate) and browser DOM are too | |
| // fragile due to whitespace/encoding differences. We trust that | |
| // pre-translate.js has updated the cache when source content changed. | |
| if (kv && !bypassCache) { | |
| try { | |
| const cached = await kv.get(cacheKey); | |
| if (cached && cached.content) { | |
| return res.json({ | |
| translatedContent: cached.content, | |
| cached: true | |
| }); | |
| } | |
| } catch (cacheError) { | |
| console.error('KV Cache Read Error:', cacheError.message); | |
| // Continue to translation if cache fails | |
| } | |
| } | |
| // ======================================================================== | |
| // STEP 2: Call Claude API | |
| // ======================================================================== | |
| const apiKey = process.env.ANTHROPIC_API_KEY; | |
| if (!apiKey) { | |
| return res.status(500).json({ error: 'ANTHROPIC_API_KEY not configured' }); | |
| } | |
| const client = new Anthropic({ apiKey }); | |
| // Construct translation prompt | |
| const prompt = `Translate this website content JSON into ${targetLanguage}. | |
| CRITICAL - YOU MUST FOLLOW THESE RULES EXACTLY: | |
| 1. Output ONLY valid JSON - no markdown code fences, no explanation | |
| 2. ALL quotes inside translated text MUST be escaped with backslash: \\" | |
| Example: "Click \\"Submit\\" to continue" NOT "Click "Submit" to continue" | |
| 3. Keep all JSON keys exactly as provided (do not translate keys like "ow-128") | |
| 4. Preserve all HTML tags exactly as they appear | |
| 5. Do not translate: "misterburton", "Burton Rast", "Tantara", "ElevenLabs", "Vercel", "Claude" | |
| Input JSON: | |
| ${JSON.stringify(content)}`; | |
| try { | |
| const message = await client.messages.create({ | |
| model: "claude-opus-4-5-20251101", | |
| max_tokens: 16000, | |
| messages: [ | |
| { role: "user", content: prompt } | |
| ] | |
| }); | |
| let text = message.content[0].text.trim(); | |
| // ==================================================================== | |
| // STEP 3: Parse Response | |
| // Claude sometimes wraps JSON in markdown fences - strip them | |
| // ==================================================================== | |
| if (text.startsWith('```')) { | |
| text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim(); | |
| } | |
| // Extract JSON object if there's extra text | |
| const firstBrace = text.indexOf('{'); | |
| const lastBrace = text.lastIndexOf('}'); | |
| if (firstBrace !== -1 && lastBrace !== -1) { | |
| text = text.substring(firstBrace, lastBrace + 1); | |
| } | |
| // Fix common JSON issues from LLM output | |
| // Remove trailing commas before } or ] | |
| text = text.replace(/,(\s*[\]\}])/g, '$1'); | |
| let translatedContent; | |
| try { | |
| translatedContent = JSON.parse(text); | |
| } catch (parseError) { | |
| console.error('JSON Parse Error. Raw text:', text); | |
| throw new Error(`JSON parse failed: ${parseError.message}`); | |
| } | |
| // ==================================================================== | |
| // STEP 4: Save to Vercel KV Cache | |
| // ==================================================================== | |
| if (kv) { | |
| try { | |
| await kv.set(cacheKey, { | |
| hash: contentHash, | |
| content: translatedContent | |
| }); | |
| } catch (cacheError) { | |
| console.error('KV Cache Write Error:', cacheError.message); | |
| // Don't fail the request if caching fails | |
| } | |
| } | |
| res.json({ | |
| translatedContent, | |
| cached: false | |
| }); | |
| } catch (error) { | |
| console.error('Claude API error:', error); | |
| res.status(500).json({ error: 'Translation failed', details: error.message }); | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment