Complete Express.js tutorial with SocketsIO Translation API — i18n best practices, caching, batch translation, and production-ready code.
Adding multilingual support to a Node.js app doesn't have to be complicated or expensive. In this tutorial, we'll build a complete Express.js application with SocketsIO Translation API — covering everything from basic translation to production-ready caching and batch processing.
mkdir multilingual-app && cd multilingual-app
npm init -y
npm install express axios dotenv node-cache
Create a .env file:
SOCKETSIO_API_KEY=your_api_key_here
PORT=3000
Let's start with a simple translation helper:
// lib/translate.js
const axios = require('axios');
const API_BASE = 'https://api.socketsio.com/v1';
async function translate(text, targetLang, sourceLang = null) {
const payload = {
q: text,
target: targetLang,
};
if (sourceLang) payload.source = sourceLang;
const response = await axios.post(`${API_BASE}/translate`, payload, {
headers: {
'Authorization': `Bearer ${process.env.SOCKETSIO_API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data.data.translations[0].translatedText;
}
async function detectLanguage(text) {
const response = await axios.post(`${API_BASE}/detect`, { q: text }, {
headers: {
'Authorization': `Bearer ${process.env.SOCKETSIO_API_KEY}`,
},
});
return response.data.data.detections[0][0].language;
}
module.exports = { translate, detectLanguage };
Test it works:
// test.js
require('dotenv').config();
const { translate, detectLanguage } = require('./lib/translate');
async function main() {
const result = await translate('Hello, world!', 'es');
console.log(result); // → "¡Hola, mundo!"
const lang = await detectLanguage('Bonjour le monde');
console.log(lang); // → "fr"
}
main();
Now let's build an Express middleware that automatically detects the user's preferred language and makes translation available in route handlers:
// middleware/i18n.js
const { detectLanguage, translate } = require('../lib/translate');
const SUPPORTED_LANGUAGES = ['en', 'es', 'fr', 'de', 'ja', 'zh', 'pt', 'ar', 'hi', 'ko'];
const DEFAULT_LANGUAGE = 'en';
function getPreferredLanguage(req) {
// 1. Check query param: ?lang=es
if (req.query.lang && SUPPORTED_LANGUAGES.includes(req.query.lang)) {
return req.query.lang;
}
// 2. Check Accept-Language header
const acceptLang = req.headers['accept-language'];
if (acceptLang) {
const preferred = acceptLang.split(',')[0].split('-')[0].trim();
if (SUPPORTED_LANGUAGES.includes(preferred)) return preferred;
}
return DEFAULT_LANGUAGE;
}
function i18nMiddleware(req, res, next) {
req.lang = getPreferredLanguage(req);
// Helper: translate a string to the user's language
req.t = async (text, sourceLang = 'en') => {
if (req.lang === sourceLang) return text;
return translate(text, req.lang, sourceLang);
};
next();
}
module.exports = i18nMiddleware;
// app.js
require('dotenv').config();
const express = require('express');
const i18n = require('./middleware/i18n');
const app = express();
app.use(express.json());
app.use(i18n);
app.get('/hello', async (req, res) => {
const message = await req.t('Welcome to our app! We support 195 languages.');
res.json({ message, language: req.lang });
});
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
curl "http://localhost:3000/hello?lang=ja" → returns the message in Japanese.
Translating the same string repeatedly wastes API credits. Add caching to avoid redundant calls:
// lib/translate-cached.js
const NodeCache = require('node-cache');
const { translate: rawTranslate, detectLanguage } = require('./translate');
// In-memory cache: 24 hour TTL
const cache = new NodeCache({ stdTTL: 86400, checkperiod: 3600 });
function cacheKey(text, target, source) {
return `${source || 'auto'}:${target}:${Buffer.from(text).toString('base64').slice(0, 32)}`;
}
async function translate(text, targetLang, sourceLang = null) {
const key = cacheKey(text, targetLang, sourceLang);
// Check cache first
const cached = cache.get(key);
if (cached !== undefined) {
return cached;
}
// Cache miss — call API
const result = await rawTranslate(text, targetLang, sourceLang);
cache.set(key, result);
return result;
}
// Cache stats endpoint
function getCacheStats() {
return cache.getStats();
}
module.exports = { translate, detectLanguage, getCacheStats };
For production, use Redis for shared caching across multiple Node.js instances:
// lib/translate-redis.js
const redis = require('redis');
const { translate: rawTranslate } = require('./translate');
const client = redis.createClient({ url: process.env.REDIS_URL });
client.connect();
const CACHE_TTL = 86400; // 24 hours
async function translate(text, targetLang, sourceLang = null) {
const key = `sio:${sourceLang || 'auto'}:${targetLang}:${text.slice(0, 100)}`;
// Try Redis cache
const cached = await client.get(key);
if (cached) return cached;
// Cache miss
const result = await rawTranslate(text, targetLang, sourceLang);
await client.setEx(key, CACHE_TTL, result);
return result;
}
module.exports = { translate };
When you need to translate multiple strings (e.g., a product catalog), use batch translation to reduce API calls and latency:
// lib/batch-translate.js
const axios = require('axios');
const API_BASE = 'https://api.socketsio.com/v1';
/**
* Translate multiple strings in a single API call.
* @param {string[]} texts - Array of strings to translate
* @param {string} targetLang - Target language code
* @param {string|null} sourceLang - Source language (null = auto-detect)
* @returns {Promise} - Array of translated strings
*/
async function batchTranslate(texts, targetLang, sourceLang = null) {
const payload = {
q: texts, // Pass array directly
target: targetLang,
};
if (sourceLang) payload.source = sourceLang;
const response = await axios.post(`${API_BASE}/translate`, payload, {
headers: {
'Authorization': `Bearer ${process.env.SOCKETSIO_API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data.data.translations.map(t => t.translatedText);
}
// Example: translate a product catalog
async function translateProductCatalog(products, targetLang) {
const names = products.map(p => p.name);
const descriptions = products.map(p => p.description);
// Two batch calls instead of 2N individual calls
const [translatedNames, translatedDescs] = await Promise.all([
batchTranslate(names, targetLang),
batchTranslate(descriptions, targetLang),
]);
return products.map((product, i) => ({
...product,
name: translatedNames[i],
description: translatedDescs[i],
lang: targetLang,
}));
}
module.exports = { batchTranslate, translateProductCatalog };
// Usage example
const { translateProductCatalog } = require('./lib/batch-translate');
const products = [
{ id: 1, name: 'Wireless Headphones', description: 'Premium audio quality' },
{ id: 2, name: 'Laptop Stand', description: 'Ergonomic aluminum design' },
{ id: 3, name: 'USB-C Hub', description: '7-in-1 connectivity solution' },
];
const spanishProducts = await translateProductCatalog(products, 'es');
// Returns all 3 products with Spanish name + description
// Only 2 API calls instead of 6!
Let users write in their native language and auto-detect it:
// routes/support.js
const express = require('express');
const { detectLanguage, translate } = require('../lib/translate-cached');
const router = express.Router();
// Support ticket endpoint — auto-detects language, translates to English for agents
router.post('/ticket', async (req, res) => {
const { message, email } = req.body;
// Detect what language the user wrote in
const detectedLang = await detectLanguage(message);
let englishMessage = message;
if (detectedLang !== 'en') {
// Translate to English for support agents
englishMessage = await translate(message, 'en', detectedLang);
}
// Store ticket with both versions
const ticket = {
id: Date.now(),
email,
originalMessage: message,
originalLanguage: detectedLang,
englishMessage,
createdAt: new Date().toISOString(),
};
// ... save to database
// Send confirmation in user's language
const confirmation = await translate(
'Your support ticket has been received. We will respond within 24 hours.',
detectedLang,
'en'
);
res.json({ ticketId: ticket.id, message: confirmation });
});
module.exports = router;
UI strings (buttons, labels, error messages) are translated thousands of times. Cache them for 24+ hours. Dynamic content (user-generated text) can have shorter TTLs.
Always pass source when you know the source language. Auto-detection adds ~20ms latency and costs extra characters for the detection call.
// Translate all UI strings at app startup
const UI_STRINGS = require('./locales/en.json');
async function preloadTranslations(languages) {
const translations = {};
for (const lang of languages) {
const keys = Object.keys(UI_STRINGS);
const values = Object.values(UI_STRINGS);
const translated = await batchTranslate(values, lang, 'en');
translations[lang] = Object.fromEntries(keys.map((k, i) => [k, translated[i]]));
}
return translations;
}
// At startup:
const TRANSLATIONS = await preloadTranslations(['es', 'fr', 'de', 'ja', 'zh']);
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur', 'yi'];
app.use((req, res, next) => {
res.locals.dir = RTL_LANGUAGES.includes(req.lang) ? 'rtl' : 'ltr';
next();
});
async function translateWithFallback(text, targetLang, fallback = text) {
try {
return await translate(text, targetLang);
} catch (err) {
console.error('Translation failed:', err.message);
return fallback; // Return original text on failure
}
}
| Concern | Recommendation |
|---|---|
| API key security | Store in environment variables, never in code |
| Rate limiting | Add request queuing for burst traffic |
| Caching | Redis for multi-instance deployments |
| Error handling | Always fall back to original text |
| Cost monitoring | Track character usage via /v1/usage endpoint |
| Language detection | Cache detected languages per user session |
500,000 free characters every month. No credit card. Works with Node.js, Python, PHP, and any HTTP client.
Get Your Free API Key →Or try the interactive playground first — no signup needed.