Back to Blog

Serverless Translation API: Build Custom Localization Workflows

Learn to build serverless translation APIs using AWS Lambda, Google Cloud Functions, and Vercel. Create custom localization workflows with automated translation pipelines.

Posted by

Serverless translation API guide

Table of Contents

Serverless Translation Architecture

Serverless translation APIs provide scalable, cost-effective solutions for automating localization workflows. By leveraging cloud functions, you can build custom translation pipelines that integrate seamlessly with your development processes, CI/CD workflows, and content management systems.

This guide demonstrates how to build production-ready serverless translation APIs using popular cloud platforms, with practical examples for common localization scenarios.

Benefits of Serverless Translation APIs

  • Scalability: Automatically scales with demand
  • Cost Efficiency: Pay only for actual usage
  • Integration: Easy integration with existing workflows
  • Maintenance: No server management required
  • Global Distribution: Deploy functions globally for low latency

Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Client App    │───▶│  Serverless API  │───▶│  Translation    │
│                 │    │                  │    │  Service        │
└─────────────────┘    └──────────────────┘    └─────────────────┘
                              │
                              ▼
                       ┌──────────────────┐
                       │   File Storage   │
                       │   (S3/GCS)       │
                       └──────────────────┘

Building with AWS Lambda

Basic Lambda Function Setup

// serverless.yml
service: translation-api

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    DEEPL_API_KEY: ${env:DEEPL_API_KEY}
    S3_BUCKET: ${env:S3_BUCKET}

functions:
  translateFile:
    handler: src/translate.handler
    events:
      - http:
          path: /translate
          method: post
          cors: true
    timeout: 300 # 5 minutes for large files

  getTranslationStatus:
    handler: src/status.handler  
    events:
      - http:
          path: /status/{id}
          method: get
          cors: true

plugins:
  - serverless-offline

Translation Handler Implementation

// src/translate.js
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

const s3 = new AWS.S3();
const dynamodb = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
  try {
    const { sourceLanguage, targetLanguages, fileContent, fileName } = JSON.parse(event.body);
    
    // Validate input
    if (!sourceLanguage || !targetLanguages || !fileContent) {
      return {
        statusCode: 400,
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ error: 'Missing required parameters' })
      };
    }

    const jobId = uuidv4();
    
    // Store original file in S3
    const s3Key = `originals/${jobId}/${fileName}`;
    await s3.putObject({
      Bucket: process.env.S3_BUCKET,
      Key: s3Key,
      Body: fileContent,
      ContentType: 'application/json'
    }).promise();

    // Create job record in DynamoDB
    await dynamodb.put({
      TableName: 'TranslationJobs',
      Item: {
        jobId,
        status: 'processing',
        sourceLanguage,
        targetLanguages,
        fileName,
        createdAt: new Date().toISOString(),
        s3Key
      }
    }).promise();

    // Start async translation process
    await startTranslationProcess(jobId, sourceLanguage, targetLanguages, fileContent);

    return {
      statusCode: 202,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ 
        jobId,
        status: 'processing',
        message: 'Translation job started'
      })
    };

  } catch (error) {
    console.error('Translation error:', error);
    return {
      statusCode: 500,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
};

const startTranslationProcess = async (jobId, sourceLanguage, targetLanguages, fileContent) => {
  // Parse JSON content
  const sourceTranslations = JSON.parse(fileContent);
  
  // Process each target language
  for (const targetLang of targetLanguages) {
    try {
      const translatedContent = await translateObject(sourceTranslations, sourceLanguage, targetLang);
      
      // Store translated file
      const translatedKey = `translations/${jobId}/${targetLang}.json`;
      await s3.putObject({
        Bucket: process.env.S3_BUCKET,
        Key: translatedKey,
        Body: JSON.stringify(translatedContent, null, 2),
        ContentType: 'application/json'
      }).promise();
      
    } catch (error) {
      console.error(`Translation failed for ${targetLang}:`, error);
    }
  }
  
  // Update job status
  await dynamodb.update({
    TableName: 'TranslationJobs',
    Key: { jobId },
    UpdateExpression: 'SET #status = :status, completedAt = :completedAt',
    ExpressionAttributeNames: { '#status': 'status' },
    ExpressionAttributeValues: {
      ':status': 'completed',
      ':completedAt': new Date().toISOString()
    }
  }).promise();
};

Translation Service Integration

// Translation service using DeepL API
const translateObject = async (obj, sourceLang, targetLang) => {
  const translated = {};
  
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'string') {
      translated[key] = await translateText(value, sourceLang, targetLang);
    } else if (typeof value === 'object' && value !== null) {
      translated[key] = await translateObject(value, sourceLang, targetLang);
    } else {
      translated[key] = value;
    }
  }
  
  return translated;
};

const translateText = async (text, sourceLang, targetLang) => {
  try {
    const response = await fetch('https://api-free.deepl.com/v2/translate', {
      method: 'POST',
      headers: {
        'Authorization': `DeepL-Auth-Key ${process.env.DEEPL_API_KEY}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        text,
        source_lang: sourceLang.toUpperCase(),
        target_lang: targetLang.toUpperCase()
      })
    });
    
    const data = await response.json();
    return data.translations[0].text;
    
  } catch (error) {
    console.error('Translation API error:', error);
    return text; // Return original text on error
  }
};

Vercel Edge Functions Implementation

Vercel Function Setup

// vercel.json
{
  "functions": {
    "api/translate.js": {
      "maxDuration": 300
    }
  },
  "env": {
    "DEEPL_API_KEY": "@deepl-api-key",
    "REDIS_URL": "@redis-url"
  }
}
// api/translate.js
import { NextRequest, NextResponse } from 'next/server';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export const config = {
  runtime: 'edge',
};

export default async function handler(req) {
  if (req.method !== 'POST') {
    return new Response('Method not allowed', { status: 405 });
  }

  try {
    const { sourceText, sourceLang, targetLang } = await req.json();
    
    // Check cache first
    const cacheKey = `translation:${sourceLang}:${targetLang}:${Buffer.from(sourceText).toString('base64')}`;
    const cached = await redis.get(cacheKey);
    
    if (cached) {
      return new Response(JSON.stringify({ 
        text: cached, 
        cached: true 
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Translate using DeepL
    const translatedText = await translateWithDeepL(sourceText, sourceLang, targetLang);
    
    // Cache result for 24 hours
    await redis.setex(cacheKey, 86400, translatedText);
    
    return new Response(JSON.stringify({ 
      text: translatedText,
      cached: false
    }), {
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    console.error('Translation error:', error);
    return new Response(JSON.stringify({ 
      error: 'Translation failed' 
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

async function translateWithDeepL(text, sourceLang, targetLang) {
  const response = await fetch('https://api-free.deepl.com/v2/translate', {
    method: 'POST',
    headers: {
      'Authorization': `DeepL-Auth-Key ${process.env.DEEPL_API_KEY}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      text,
      source_lang: sourceLang.toUpperCase(),
      target_lang: targetLang.toUpperCase()
    })
  });

  const data = await response.json();
  return data.translations[0].text;
}

Batch Translation Function

// api/translate-batch.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { translations, sourceLang, targetLang } = req.body;
  
  try {
    const results = {};
    
    // Process translations in batches to avoid rate limits
    const batchSize = 10;
    const batches = [];
    
    for (let i = 0; i < Object.keys(translations).length; i += batchSize) {
      const batch = Object.entries(translations).slice(i, i + batchSize);
      batches.push(batch);
    }
    
    for (const batch of batches) {
      const batchPromises = batch.map(async ([key, text]) => {
        const translatedText = await translateWithDeepL(text, sourceLang, targetLang);
        return [key, translatedText];
      });
      
      const batchResults = await Promise.all(batchPromises);
      
      batchResults.forEach(([key, translatedText]) => {
        results[key] = translatedText;
      });
      
      // Small delay between batches
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    
    res.status(200).json({ translations: results });
    
  } catch (error) {
    console.error('Batch translation error:', error);
    res.status(500).json({ error: 'Batch translation failed' });
  }
}

Google Cloud Functions Approach

Cloud Function Setup

// package.json
{
  "name": "translation-function",
  "version": "1.0.0",
  "dependencies": {
    "@google-cloud/functions-framework": "^3.0.0",
    "@google-cloud/storage": "^6.0.0",
    "@google-cloud/firestore": "^6.0.0",
    "node-fetch": "^3.0.0"
  }
}

// index.js
const functions = require('@google-cloud/functions-framework');
const { Storage } = require('@google-cloud/storage');
const { Firestore } = require('@google-cloud/firestore');

const storage = new Storage();
const firestore = new Firestore();

functions.http('translateFile', async (req, res) => {
  // Enable CORS
  res.set('Access-Control-Allow-Origin', '*');
  
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    res.status(204).send('');
    return;
  }

  if (req.method !== 'POST') {
    res.status(405).send('Method Not Allowed');
    return;
  }

  try {
    const { fileContent, sourceLang, targetLangs, fileName } = req.body;
    
    const jobId = generateJobId();
    
    // Store job in Firestore
    await firestore.collection('translation-jobs').doc(jobId).set({
      status: 'processing',
      sourceLang,
      targetLangs,
      fileName,
      createdAt: new Date(),
      progress: 0
    });

    // Process translation asynchronously
    processTranslation(jobId, fileContent, sourceLang, targetLangs, fileName)
      .catch(error => console.error('Translation processing error:', error));

    res.status(202).json({
      jobId,
      status: 'processing',
      statusUrl: `/status/${jobId}`
    });

  } catch (error) {
    console.error('Function error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function processTranslation(jobId, fileContent, sourceLang, targetLangs, fileName) {
  try {
    const sourceData = JSON.parse(fileContent);
    const results = {};
    
    for (let i = 0; i < targetLangs.length; i++) {
      const targetLang = targetLangs[i];
      
      // Update progress
      await firestore.collection('translation-jobs').doc(jobId).update({
        progress: Math.round((i / targetLangs.length) * 100)
      });
      
      const translatedData = await translateObject(sourceData, sourceLang, targetLang);
      results[targetLang] = translatedData;
      
      // Store translated file in Cloud Storage
      const bucket = storage.bucket('translation-results');
      const file = bucket.file(`${jobId}/${targetLang}.json`);
      
      await file.save(JSON.stringify(translatedData, null, 2), {
        metadata: {
          contentType: 'application/json'
        }
      });
    }
    
    // Mark job as completed
    await firestore.collection('translation-jobs').doc(jobId).update({
      status: 'completed',
      progress: 100,
      completedAt: new Date(),
      downloadUrls: Object.keys(results).reduce((urls, lang) => {
        urls[lang] = `/download/${jobId}/${lang}.json`;
        return urls;
      }, {})
    });
    
  } catch (error) {
    console.error('Translation processing error:', error);
    
    await firestore.collection('translation-jobs').doc(jobId).update({
      status: 'failed',
      error: error.message
    });
  }
}

Automated Translation Workflows

GitHub Actions Integration

# .github/workflows/translate.yml
name: Auto-translate on content changes

on:
  push:
    paths:
      - 'src/locales/en/**'
  
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Trigger translation API
        run: |
          curl -X POST ${{ secrets.TRANSLATION_API_URL }}/translate \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
            -d '{
              "sourceFiles": "src/locales/en/**/*.json",
              "targetLanguages": ["es", "fr", "de", "it"],
              "webhook": "${{ secrets.WEBHOOK_URL }}"
            }'
            
      - name: Wait for completion and download
        run: node scripts/download-translations.js
        
      - name: Commit translated files
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add src/locales/
          git commit -m "Auto-update translations" || exit 0
          git push

Webhook Handler for CI/CD

// api/webhook.js
import { NextRequest } from 'next/server';
import { Octokit } from '@octokit/rest';

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN
});

export default async function handler(req) {
  if (req.method !== 'POST') {
    return new Response('Method not allowed', { status: 405 });
  }

  try {
    const { jobId, status, downloadUrls } = await req.json();
    
    if (status === 'completed') {
      // Download translation files
      const translations = await downloadTranslationFiles(downloadUrls);
      
      // Create pull request with translations
      await createTranslationPR(jobId, translations);
      
      return new Response(JSON.stringify({ 
        message: 'Pull request created successfully' 
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return new Response(JSON.stringify({ 
      message: 'Webhook received' 
    }), {
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    console.error('Webhook error:', error);
    return new Response(JSON.stringify({ 
      error: 'Webhook processing failed' 
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

async function createTranslationPR(jobId, translations) {
  const owner = process.env.GITHUB_OWNER;
  const repo = process.env.GITHUB_REPO;
  const branchName = `translations/${jobId}`;
  
  // Create branch
  const { data: mainBranch } = await octokit.rest.git.getRef({
    owner,
    repo,
    ref: 'heads/main'
  });
  
  await octokit.rest.git.createRef({
    owner,
    repo,
    ref: `refs/heads/${branchName}`,
    sha: mainBranch.object.sha
  });
  
  // Commit translation files
  for (const [language, content] of Object.entries(translations)) {
    await octokit.rest.repos.createOrUpdateFileContents({
      owner,
      repo,
      path: `src/locales/${language}/translation.json`,
      message: `Add ${language} translations`,
      content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
      branch: branchName
    });
  }
  
  // Create pull request
  await octokit.rest.pulls.create({
    owner,
    repo,
    title: `Auto-generated translations (${jobId})`,
    head: branchName,
    base: 'main',
    body: `Automated translation update generated by serverless API.\n\nJob ID: ${jobId}`
  });
}

Monitoring and Scaling

Performance Monitoring

// monitoring/metrics.js
import { CloudWatch } from '@aws-sdk/client-cloudwatch';

const cloudwatch = new CloudWatch({ region: 'us-east-1' });

export const recordMetric = async (metricName, value, unit = 'Count') => {
  try {
    await cloudwatch.putMetricData({
      Namespace: 'TranslationAPI',
      MetricData: [{
        MetricName: metricName,
        Value: value,
        Unit: unit,
        Timestamp: new Date()
      }]
    });
  } catch (error) {
    console.error('Failed to record metric:', error);
  }
};

// Usage in Lambda function
exports.handler = async (event) => {
  const startTime = Date.now();
  
  try {
    // ... translation logic
    
    await recordMetric('TranslationSuccess', 1);
    await recordMetric('TranslationLatency', Date.now() - startTime, 'Milliseconds');
    
  } catch (error) {
    await recordMetric('TranslationError', 1);
    throw error;
  }
};

Rate Limiting and Quotas

// utils/rateLimit.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

const rateLimiter = {
  async checkLimit(userId, limit = 100, window = 3600) {
    const key = `rate_limit:${userId}:${Math.floor(Date.now() / (window * 1000))}`;
    
    const current = await redis.incr(key);
    
    if (current === 1) {
      await redis.expire(key, window);
    }
    
    return {
      allowed: current <= limit,
      remaining: Math.max(0, limit - current),
      resetTime: Math.ceil(Date.now() / 1000) + window
    };
  }
};

// Usage in API handler
exports.handler = async (event) => {
  const userId = event.requestContext.identity.sourceIp;
  const { allowed, remaining, resetTime } = await rateLimiter.checkLimit(userId);
  
  if (!allowed) {
    return {
      statusCode: 429,
      headers: {
        'X-RateLimit-Remaining': remaining,
        'X-RateLimit-Reset': resetTime
      },
      body: JSON.stringify({ error: 'Rate limit exceeded' })
    };
  }
  
  // ... rest of the handler
};

Real-World Integration Examples

Content Management System Integration

// CMS plugin example (Strapi)
module.exports = {
  async afterCreate(event) {
    const { result } = event;
    
    if (result.locale === 'en') {
      // Trigger translation for new English content
      await fetch(process.env.TRANSLATION_API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          contentId: result.id,
          contentType: 'article',
          fields: ['title', 'description', 'content'],
          targetLanguages: ['es', 'fr', 'de']
        })
      });
    }
  }
};

E-commerce Platform Integration

// Shopify app webhook handler
app.post('/webhook/products/create', async (req, res) => {
  const product = req.body;
  
  // Extract translatable content
  const translatableFields = {
    title: product.title,
    description: product.body_html,
    tags: product.tags,
    seo_title: product.seo_title,
    seo_description: product.seo_description
  };
  
  // Send to translation API
  const response = await fetch(process.env.TRANSLATION_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.API_TOKEN}`
    },
    body: JSON.stringify({
      sourceContent: translatableFields,
      sourceLang: 'en',
      targetLangs: ['es', 'fr', 'de'],
      webhook: `${process.env.BASE_URL}/webhook/translation-complete`,
      metadata: {
        productId: product.id,
        type: 'product'
      }
    })
  });
  
  res.status(200).send('OK');
});

Documentation Site Integration

// Docusaurus plugin
module.exports = function(context, options) {
  return {
    name: 'auto-translate-plugin',
    
    async postBuild({ siteConfig, routesPaths, outDir }) {
      const { targetLanguages = [] } = options;
      
      if (targetLanguages.length === 0) return;
      
      // Find all markdown files
      const markdownFiles = await glob('docs/**/*.md');
      
      for (const file of markdownFiles) {
        const content = await fs.readFile(file, 'utf8');
        const { data: frontmatter, content: body } = matter(content);
        
        // Send to translation API
        await fetch(process.env.TRANSLATION_API_URL, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            frontmatter,
            body,
            filePath: file,
            targetLanguages
          })
        });
      }
    }
  };
};

Leveraging i18nowAI for Serverless Workflows

While building custom serverless translation APIs provides flexibility, i18nowAI offers a production-ready alternative that eliminates the complexity of managing translation infrastructure while providing superior translation quality.

i18nowAI API Integration

// Simplified serverless function using i18nowAI
export default async function handler(req, res) {
  const { sourceFile, targetLanguages } = req.body;
  
  try {
    // Upload to i18nowAI
    const response = await fetch('https://api.i18now.ai/translate', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.I18NOWAI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        source: sourceFile,
        targets: targetLanguages,
        format: 'json'
      })
    });
    
    const result = await response.json();
    
    res.status(200).json({
      jobId: result.jobId,
      status: 'processing',
      webhook: result.webhookUrl
    });
    
  } catch (error) {
    res.status(500).json({ error: 'Translation failed' });
  }
}

Benefits of i18nowAI Integration

  • No Infrastructure Management: Focus on your application, not translation infrastructure
  • Superior Quality: DeepL-powered translations with context awareness
  • Automatic Optimization: Built-in caching, rate limiting, and error handling
  • Cost Efficiency: Pay-per-use pricing without infrastructure overhead
  • Instant Scalability: Handle any volume without configuration

Best Practices for Serverless Translation APIs

  • Async Processing: Use asynchronous processing for large translation jobs
  • Error Handling: Implement comprehensive error handling and retry logic
  • Caching: Cache translations to reduce API calls and improve performance
  • Rate Limiting: Implement rate limiting to prevent abuse
  • Monitoring: Use comprehensive monitoring and alerting
  • Security: Implement proper authentication and input validation

Conclusion

Serverless translation APIs provide powerful, scalable solutions for automating localization workflows. Whether you build custom solutions with AWS Lambda, Vercel Functions, or Google Cloud Functions, the serverless approach offers cost-effective scalability and easy integration.

For teams seeking to minimize infrastructure complexity while maximizing translation quality, integrating i18nowAI into your serverless workflows provides the best of both worlds: custom automation with professional-grade translation services.

Explore our other guides on JSON translation best practices and handling complex pluralization rules to further optimize your localization workflow.