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.

Revisions

  1. misterburton revised this gist Jan 21, 2026. 1 changed file with 32 additions and 34 deletions.
    66 changes: 32 additions & 34 deletions translate-api.js
    Original file line number Diff line number Diff line change
    @@ -1,20 +1,20 @@
    /**
    * translate-api.js (Vercel Serverless Function)
    *
    * A Vercel API endpoint that translates content using Google's Gemini Flash
    * 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: @google/generative-ai, @vercel/kv
    * - npm packages: @anthropic-ai/sdk, @vercel/kv
    * - Environment variables in Vercel dashboard or .env.local:
    * - GEMINI_API_KEY: Your Google AI Studio API key
    * - 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 GEMINI_API_KEY to your Vercel environment variables
    * 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
    *
    @@ -42,7 +42,7 @@
    * @license MIT
    */

    const { GoogleGenerativeAI } = require("@google/generative-ai");
    const Anthropic = require("@anthropic-ai/sdk").default;
    const { createClient } = require("@vercel/kv");

    // Initialize KV client only if environment variables are present
    @@ -96,46 +96,43 @@ module.exports = async (req, res) => {
    }

    // ========================================================================
    // STEP 2: Call Gemini API
    // STEP 2: Call Claude API
    // ========================================================================
    const apiKey = process.env.GEMINI_API_KEY;
    const apiKey = process.env.ANTHROPIC_API_KEY;
    if (!apiKey) {
    return res.status(500).json({ error: 'GEMINI_API_KEY not configured' });
    return res.status(500).json({ error: 'ANTHROPIC_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",
    });
    const client = new Anthropic({ apiKey });

    // 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.
    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"
    ${JSON.stringify(content)}`;
    Input JSON:
    ${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 message = await client.messages.create({
    model: "claude-opus-4-5-20251101",
    max_tokens: 16000,
    messages: [
    { role: "user", content: prompt }
    ]
    });

    const response = await result.response;
    let text = response.text().trim();
    let text = message.content[0].text.trim();

    // ====================================================================
    // STEP 3: Parse Response
    // Gemini sometimes wraps JSON in markdown fences - strip them
    // Claude sometimes wraps JSON in markdown fences - strip them
    // ====================================================================
    if (text.startsWith('```')) {
    text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
    @@ -147,8 +144,9 @@ module.exports = async (req, res) => {
    if (firstBrace !== -1 && lastBrace !== -1) {
    text = text.substring(firstBrace, lastBrace + 1);
    }

    // Remove trailing commas which can break JSON.parse

    // Fix common JSON issues from LLM output
    // Remove trailing commas before } or ]
    text = text.replace(/,(\s*[\]\}])/g, '$1');

    let translatedContent;
    @@ -180,7 +178,7 @@ module.exports = async (req, res) => {
    });

    } catch (error) {
    console.error('Gemini API error:', error);
    console.error('Claude API error:', error);
    res.status(500).json({ error: 'Translation failed', details: error.message });
    }
    };
  2. misterburton created this gist Jan 6, 2026.
    186 changes: 186 additions & 0 deletions translate-api.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,186 @@
    /**
    * 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 });
    }
    };