Build a multilingual chat app where users speak different languages and understand each other in real time. Node.js + Socket.io + SocketsIO Translation API with 101ms avg latency.
Imagine building a global gaming platform, a customer support system, or a marketplace where buyers and sellers speak different languages. Real-time communication is essential — but language barriers fragment your user base into isolated groups.
The naive solution is to ask users to translate messages themselves, but that kills the real-time feel. The right solution is transparent translation middleware: messages are translated automatically before delivery, so each user reads chat in their own language without any extra steps.
This is technically challenging because:
SocketsIO Translation API is purpose-built for this use case: 101ms average latency, 195 languages, automatic language detection, and $3.50/million characters. A busy chat app sending 1,000 messages/day at 50 characters each costs just $0.175/day.
Here is the architecture we will build:
Client A (English) Server Client B (Japanese)
| | |
| send("Hello!") | |
|----------------------> | |
| | detect language: "en" |
| | translate to "ja" |
| | cache result |
| | emit to room |
| |----------------------> |
| | "こんにちは!" |
| | |
| <-- emit back to sender with original --|
| "Hello!" (your own message, no translation) |
Key design decisions:
Let us build the server. We will use Express.js, Socket.io, and axios for the translation API calls.
npm init -y
npm install express socket.io axios redis
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const axios = require('axios');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: '*', methods: ['GET', 'POST'] }
});
const SOCKETSIO_API_KEY = process.env.SOCKETSIO_API_KEY || 'YOUR_API_KEY';
const SOCKETSIO_API_URL = 'https://api.socketsio.com/v1/translate';
// In-memory cache (replace with Redis in production)
const translationCache = new Map();
// Track connected users and their language preferences
const userLanguages = new Map(); // socketId -> languageCode
async function detectLanguage(text) {
// Use the translate endpoint with source=auto to detect language
const response = await axios.post(
SOCKETSIO_API_URL,
{ q: text, target: 'en', source: 'auto' },
{
headers: {
Authorization: `Bearer ${SOCKETSIO_API_KEY}`,
'Content-Type': 'application/json',
},
timeout: 5000,
}
);
// The API returns the detected source language
return response.data.data.translations[0].detectedSourceLanguage || 'en';
}
async function translateMessage(text, sourceLang, targetLang) {
if (sourceLang === targetLang) return text;
const cacheKey = `\${text}:\${sourceLang}:\${targetLang}`;
if (translationCache.has(cacheKey)) {
return translationCache.get(cacheKey);
}
const response = await axios.post(
SOCKETSIO_API_URL,
{ q: text, target: targetLang, source: sourceLang },
{
headers: {
Authorization: `Bearer ${SOCKETSIO_API_KEY}`,
'Content-Type': 'application/json',
},
timeout: 5000,
}
);
const translated = response.data.data.translations[0].translatedText;
translationCache.set(cacheKey, translated);
// Limit cache size to 10,000 entries
if (translationCache.size > 10000) {
const firstKey = translationCache.keys().next().value;
translationCache.delete(firstKey);
}
return translated;
}
io.on('connection', (socket) => {
console.log(`User connected: \${socket.id}`);
// User registers their language preference on connect
socket.on('register', ({ language, username }) => {
userLanguages.set(socket.id, { language, username });
socket.join('global-chat');
io.to('global-chat').emit('user-joined', {
username,
language,
timestamp: Date.now(),
});
console.log(`\${username} joined speaking \${language}`);
});
// Handle incoming chat message
socket.on('message', async ({ text, room = 'global-chat' }) => {
const sender = userLanguages.get(socket.id);
if (!sender) return;
const timestamp = Date.now();
const messageId = `\${socket.id}-\${timestamp}`;
// Detect the source language
let sourceLang;
try {
sourceLang = await detectLanguage(text);
} catch (err) {
sourceLang = sender.language; // Fallback to registered language
}
// Get all sockets in the room
const socketsInRoom = await io.in(room).fetchSockets();
// Translate and deliver to each recipient
const deliveryPromises = socketsInRoom.map(async (recipientSocket) => {
const recipient = userLanguages.get(recipientSocket.id);
if (!recipient) return;
let messageText = text;
let wasTranslated = false;
// Only translate if recipient speaks a different language
if (recipient.language !== sourceLang) {
try {
messageText = await translateMessage(text, sourceLang, recipient.language);
wasTranslated = true;
} catch (err) {
console.error(`Translation error for \${recipientSocket.id}:`, err.message);
// Deliver original text on translation failure
}
}
recipientSocket.emit('message', {
id: messageId,
text: messageText,
originalText: wasTranslated ? text : undefined,
originalLang: sourceLang,
sender: sender.username,
wasTranslated,
timestamp,
});
});
await Promise.all(deliveryPromises);
});
socket.on('disconnect', () => {
const user = userLanguages.get(socket.id);
if (user) {
io.to('global-chat').emit('user-left', {
username: user.username,
timestamp: Date.now(),
});
userLanguages.delete(socket.id);
}
console.log(`User disconnected: \${socket.id}`);
});
});
httpServer.listen(3000, () => {
console.log('Multilingual chat server running on http://localhost:3000');
});
Users don't always type in their registered language. A French user might type in English to be polite, or switch languages mid-conversation. The SocketsIO API detects the source language automatically when you set source: "auto":
// Detect language without translating
async function detectOnly(text) {
const response = await axios.post(
'https://api.socketsio.com/v1/translate',
{ q: text, target: 'en', source: 'auto' },
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
return response.data.data.translations[0].detectedSourceLanguage;
}
// Examples:
await detectOnly('Hello world'); // returns "en"
await detectOnly('Hola mundo'); // returns "es"
await detectOnly('こんにちは'); // returns "ja"
await detectOnly('Bonjour le monde'); // returns "fr"
await detectOnly('你好世界'); // returns "zh"
For large chat rooms with many language groups, you can optimize by translating once per target language rather than once per user:
async function broadcastTranslated(io, room, message, sourceLang, senderSocketId) {
// Group recipients by language
const socketsInRoom = await io.in(room).fetchSockets();
const languageGroups = new Map(); // language -> [socketIds]
for (const sock of socketsInRoom) {
const user = userLanguages.get(sock.id);
if (!user) continue;
if (!languageGroups.has(user.language)) {
languageGroups.set(user.language, []);
}
languageGroups.get(user.language).push(sock.id);
}
// Translate once per unique target language
const translationPromises = Array.from(languageGroups.entries()).map(
async ([lang, socketIds]) => {
let translatedText = message.text;
let wasTranslated = false;
if (lang !== sourceLang) {
try {
translatedText = await translateMessage(message.text, sourceLang, lang);
wasTranslated = true;
} catch (err) {
console.error(`Failed to translate to \${lang}:`, err.message);
}
}
// Emit to all sockets in this language group
for (const socketId of socketIds) {
io.to(socketId).emit('message', {
...message,
text: translatedText,
originalText: wasTranslated ? message.text : undefined,
wasTranslated,
});
}
}
);
await Promise.all(translationPromises);
}
// This approach translates once per language, not once per user.
// For a room with 1000 users speaking 10 languages, this means
// 10 API calls instead of 1000 — a 100x efficiency gain.
Here is a minimal React chat client that connects to our translation server:
// ChatApp.jsx
import { useState, useEffect, useRef } from 'react';
import { io } from 'socket.io-client';
const LANGUAGES = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'fr', name: 'Français' },
{ code: 'de', name: 'Deutsch' },
{ code: 'ja', name: '日本語' },
{ code: 'zh', name: '中文' },
{ code: 'ko', name: '한국어' },
{ code: 'pt', name: 'Português' },
];
export default function ChatApp() {
const [socket, setSocket] = useState(null);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [username, setUsername] = useState('');
const [language, setLanguage] = useState('en');
const [joined, setJoined] = useState(false);
const messagesEndRef = useRef(null);
useEffect(() => {
const newSocket = io('http://localhost:3000');
setSocket(newSocket);
newSocket.on('message', (msg) => {
setMessages((prev) => [...prev, msg]);
});
newSocket.on('user-joined', ({ username, language }) => {
setMessages((prev) => [
...prev,
{ id: Date.now(), text: `\${username} joined (speaks \${language})`, isSystem: true },
]);
});
newSocket.on('user-left', ({ username }) => {
setMessages((prev) => [
...prev,
{ id: Date.now(), text: `\${username} left`, isSystem: true },
]);
});
return () => newSocket.disconnect();
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const joinChat = () => {
if (!username.trim() || !socket) return;
socket.emit('register', { language, username });
setJoined(true);
};
const sendMessage = (e) => {
e.preventDefault();
if (!input.trim() || !socket) return;
socket.emit('message', { text: input });
setInput('');
};
if (!joined) {
return (
<div className="join-screen">
<h1>Multilingual Chat</h1>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Your name"
/>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
{LANGUAGES.map((l) => (
<option key={l.code} value={l.code}>{l.name}</option>
))}
</select>
<button onClick={joinChat}>Join Chat</button>
</div>
);
}
return (
<div className="chat-app">
<div className="messages">
{messages.map((msg) => (
<div key={msg.id} className={msg.isSystem ? 'system-msg' : 'chat-msg'}>
{!msg.isSystem && <strong>{msg.sender}: </strong>}
{msg.text}
{msg.wasTranslated && (
<span className="translated-badge" title={`Original: \${msg.originalText}`}>
🌐 translated
</span>
)}
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="message-form">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={`Type in \${LANGUAGES.find(l => l.code === language)?.name}...`}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
In production, replace the in-memory Map cache with Redis. Redis persists across server restarts and works across multiple server instances (essential for horizontal scaling):
const { createClient } = require('redis');
const redis = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
await redis.connect();
const CACHE_TTL = 60 * 60 * 24 * 7; // 7 days in seconds
async function translateWithRedisCache(text, sourceLang, targetLang) {
if (sourceLang === targetLang) return text;
const cacheKey = `translate:\${Buffer.from(text).toString('base64').slice(0, 32)}:\${sourceLang}:\${targetLang}`;
// Check Redis cache
const cached = await redis.get(cacheKey);
if (cached) {
return cached;
}
// Cache miss: call the API
const response = await axios.post(
'https://api.socketsio.com/v1/translate',
{ q: text, target: targetLang, source: sourceLang },
{ headers: { Authorization: `Bearer ${SOCKETSIO_API_KEY}` } }
);
const translated = response.data.data.translations[0].translatedText;
// Store in Redis with TTL
await redis.setEx(cacheKey, CACHE_TTL, translated);
return translated;
}
// Cache hit rates in practice:
// - Common greetings ("hello", "thanks"): >95% hit rate
// - Short messages (<20 chars): ~60-70% hit rate
// - Unique long messages: ~5-10% hit rate
// - Overall in active chat: ~40-50% hit rate
// This roughly halves your translation API costs.
Let us calculate the real cost of running a multilingual chat app with SocketsIO Translation API:
| Scenario | Messages/day | Avg chars/msg | Translations/msg | Daily cost | Monthly cost |
|---|---|---|---|---|---|
| Small app | 1,000 | 50 | 1 | $0.175 | $5.25 |
| Mid-size app | 10,000 | 60 | 2 | $4.20 | $126 |
| With Redis cache (50%) | 10,000 | 60 | 2 | $2.10 | $63 |
| Large app | 100,000 | 60 | 3 | $63 | $1,890 |
| Large + cache (50%) | 100,000 | 60 | 3 | $31.50 | $945 |
Compare this to Google Translate ($20/M chars) for the same large app with cache: $5,400/month vs $945/month with SocketsIO. That is a saving of $4,455/month — or $53,460/year.
Don't translate "is typing..." notifications — these are UI-only events that don't need translation.
function isEmojiOnly(text) {
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\s]+$/u;
return emojiRegex.test(text);
}
// Skip translation for "😂😂😂" or "👏👏"
if (isEmojiOnly(message)) {
io.to(room).emit('message', { text: message, wasTranslated: false });
return;
}
Cap chat messages at 500 characters. This keeps translation costs predictable and improves the chat experience (nobody wants to read a wall of text in a chat).
As shown earlier, group users by language and translate once per language group rather than once per user. In a room with 100 users speaking 5 languages, this reduces API calls by 95%.
500,000 characters/month free — enough for ~10,000 chat messages. No credit card required. 101ms avg latency. 195 languages. $3.50/M chars pay-as-you-go.
Get Your Free API Key →Or test instantly in the API Playground — no signup needed