Created
January 6, 2026 21:33
-
-
Save misterburton/3b4f99e1996874884833833ddd45d527 to your computer and use it in GitHub Desktop.
Vercel API endpoint for AI translation using Gemini Flash + 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 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