Skip to content

Instantly share code, notes, and snippets.

@misterburton
Last active January 21, 2026 18:10
Show Gist options
  • Select an option

  • Save misterburton/3b4f99e1996874884833833ddd45d527 to your computer and use it in GitHub Desktop.

Select an option

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