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
Related reading
Pluralization Rules in i18next: Handle Complex Grammar Across Languages
Master i18next pluralization rules for complex languages. Learn to handle multiple plural forms, gender agreement, and advanced grammar rules in your internationalized applications.
JSON Translation Files: Best Practices for App Localization
Master JSON translation file organization, naming conventions, and optimization techniques. Learn best practices for scalable app localization with practical examples and tools.
Vue.js i18n Setup: Easy Internationalization for Vue Applications
Learn how to implement Vue.js internationalization (i18n) with Vue I18n and i18next. Complete setup guide with examples, best practices, and automated translation workflows.

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-offlineTranslation 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 pushWebhook 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.
