Skip to content

Instantly share code, notes, and snippets.

@misterburton
Created January 6, 2026 21:33
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 Gemini Flash + Vercel KV caching
/**
* translate-api.js (Vercel Serverless Function)
*
* A Vercel API endpoint that translates content using Google's Gemini Flash
* and caches results in Vercel KV for fast retrieval.
*
* REQUIREMENTS:
* - Vercel project with KV database configured
* - npm packages: @google/generative-ai, @vercel/kv
* - Environment variables in Vercel dashboard or .env.local:
* - GEMINI_API_KEY: Your Google AI Studio 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 GEMINI_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 { GoogleGenerativeAI } = require("@google/generative-ai");
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 Gemini API
// ========================================================================
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: 'GEMINI_API_KEY not configured' });
}
const genAI = new GoogleGenerativeAI(apiKey);
// Initialize Gemini 3 Flash model
// You can also use "gemini-2.0-flash" or other available models
const model = genAI.getGenerativeModel({
model: "gemini-3-flash-preview",
});
// Construct translation prompt
const prompt = `Translate this website content JSON into ${targetLanguage}.
Maintain all HTML tags and JSON keys exactly. Do not translate brand names.
Respond ONLY with the translated JSON object.
${JSON.stringify(content)}`;
try {
const result = await model.generateContent({
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.1, // Low temperature for consistent translations
maxOutputTokens: 1000000,
thinkingConfig: {
thinkingLevel: "minimal" // Reduce latency
}
}
});
const response = await result.response;
let text = response.text().trim();
// ====================================================================
// STEP 3: Parse Response
// Gemini 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);
}
// Remove trailing commas which can break JSON.parse
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('Gemini 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