Add real-time multilingual support to your Flutter app. Full Dart code examples, caching with SharedPreferences, error handling, and a language switcher widget — all with 500K free characters/month.
Flutter has become the go-to framework for cross-platform mobile development, powering apps for Android, iOS, web, and desktop from a single codebase. But shipping a Flutter app to a global audience means more than just responsive layouts — it means speaking your users' language.
Consider the numbers: over 7.5 billion people in the world, and only about 1.5 billion speak English as a first or second language. The Google Play Store and Apple App Store together serve users in 190+ countries. Apps that support local languages see dramatically higher conversion rates, lower churn, and better store ratings.
Flutter's built-in flutter_localizations package handles static string localization well — but it requires you to pre-translate every string at build time. That approach breaks down when you need to:
This is where a translation API comes in. SocketsIO Translation API supports 195 languages, is Google Translate v2 API compatible, and offers 500,000 characters/month free with no credit card required. At $3.50 per million characters pay-as-you-go, it's 83% cheaper than Google ($20/M) and 86% cheaper than DeepL ($25/M).
Before writing any Dart code, you need to add the required dependencies to your pubspec.yaml. We'll use the http package for API calls and shared_preferences for caching translated strings locally.
name: my_multilingual_app
description: A Flutter app with SocketsIO translation API integration
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.10.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
shared_preferences: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
After updating pubspec.yaml, run:
flutter pub get
You'll also need an API key. Sign up at socketsio.com/signup.html — the free tier gives you 500,000 characters/month with no credit card required.
The core of our integration is a TranslationService Dart class that wraps the SocketsIO API. This service handles HTTP requests, JSON parsing, error handling, and caching.
// lib/services/translation_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class TranslationService {
static const String _baseUrl = 'https://api.socketsio.com/v1/translate';
static const String _apiKey = 'YOUR_SOCKETSIO_API_KEY'; // Replace with your key
static const Duration _timeout = Duration(seconds: 10);
final http.Client _client;
TranslationService({http.Client? client}) : _client = client ?? http.Client();
/// Translate a single string from [source] language to [target] language.
/// Set [source] to 'auto' for automatic language detection.
Future<String> translate(
String text, {
required String target,
String source = 'auto',
}) async {
if (text.trim().isEmpty) return text;
final body = jsonEncode({
'q': text,
'target': target,
'source': source,
});
final response = await _client
.post(
Uri.parse(_baseUrl),
headers: {
'Authorization': 'Bearer $_apiKey',
'Content-Type': 'application/json',
},
body: body,
)
.timeout(_timeout);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return data['data']['translations'][0]['translatedText'] as String;
} else if (response.statusCode == 401) {
throw TranslationException('Invalid API key. Check your SocketsIO API key.');
} else if (response.statusCode == 429) {
throw TranslationException('Rate limit exceeded. Please wait before retrying.');
} else {
throw TranslationException('Translation failed: HTTP \${response.statusCode}');
}
}
/// Translate multiple strings in a single API call.
/// Returns a list of translated strings in the same order as [texts].
Future<List<String>> translateBatch(
List<String> texts, {
required String target,
String source = 'auto',
}) async {
if (texts.isEmpty) return [];
// Filter empty strings but preserve positions
final nonEmpty = texts.asMap().entries.where((e) => e.value.trim().isNotEmpty).toList();
if (nonEmpty.isEmpty) return texts;
final body = jsonEncode({
'q': nonEmpty.map((e) => e.value).toList(),
'target': target,
'source': source,
});
final response = await _client
.post(
Uri.parse(_baseUrl),
headers: {
'Authorization': 'Bearer $_apiKey',
'Content-Type': 'application/json',
},
body: body,
)
.timeout(_timeout);
if (response.statusCode != 200) {
throw TranslationException('Batch translation failed: HTTP \${response.statusCode}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final translations = data['data']['translations'] as List;
final translatedTexts = translations.map((t) => t['translatedText'] as String).toList();
// Reconstruct the full list preserving original positions
final result = List<String>.from(texts);
for (var i = 0; i < nonEmpty.length; i++) {
result[nonEmpty[i].key] = translatedTexts[i];
}
return result;
}
void dispose() {
_client.close();
}
}
class TranslationException implements Exception {
final String message;
TranslationException(this.message);
@override
String toString() => 'TranslationException: \$message';
}
Once you have the TranslationService, you can use it anywhere in your widget tree. Here's how to translate UI strings when a user changes their language preference:
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../services/translation_service.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _translationService = TranslationService();
String _currentLang = 'en';
bool _isLoading = false;
// UI strings to translate
Map<String, String> _strings = {
'welcome': 'Welcome to our app',
'subtitle': 'Discover amazing features',
'cta': 'Get Started',
'login': 'Already have an account? Log in',
};
Future<void> _translateTo(String langCode) async {
if (langCode == 'en') {
setState(() {
_strings = {
'welcome': 'Welcome to our app',
'subtitle': 'Discover amazing features',
'cta': 'Get Started',
'login': 'Already have an account? Log in',
};
_currentLang = 'en';
});
return;
}
setState(() => _isLoading = true);
try {
final keys = _strings.keys.toList();
final values = _strings.values.toList();
final translated = await _translationService.translateBatch(
values,
target: langCode,
source: 'en',
);
setState(() {
_strings = Map.fromIterables(keys, translated);
_currentLang = langCode;
_isLoading = false;
});
} on TranslationException catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Translation error: \${e.message}')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multilingual App'),
actions: [
LanguageSwitcher(
currentLang: _currentLang,
onLanguageChanged: _translateTo,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_strings['welcome']!,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(_strings['subtitle']!),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {},
child: Text(_strings['cta']!),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {},
child: Text(_strings['login']!),
),
],
),
),
);
}
@override
void dispose() {
_translationService.dispose();
super.dispose();
}
}
Making an API call every time a user opens your app wastes bandwidth and adds latency. A simple caching layer using SharedPreferences stores translations locally so they're available instantly on subsequent launches.
// lib/services/cached_translation_service.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'translation_service.dart';
class CachedTranslationService {
final TranslationService _service;
static const String _cachePrefix = 'translation_cache_';
static const Duration _cacheTtl = Duration(days: 7);
CachedTranslationService({TranslationService? service})
: _service = service ?? TranslationService();
String _cacheKey(String text, String target, String source) {
// Create a deterministic cache key
final hash = '\${text.hashCode}_\${target}_\${source}';
return '\$_cachePrefix\$hash';
}
Future<String> translate(
String text, {
required String target,
String source = 'auto',
}) async {
final prefs = await SharedPreferences.getInstance();
final key = _cacheKey(text, target, source);
// Check cache
final cached = prefs.getString(key);
if (cached != null) {
final entry = jsonDecode(cached) as Map<String, dynamic>;
final cachedAt = DateTime.parse(entry['cachedAt'] as String);
if (DateTime.now().difference(cachedAt) < _cacheTtl) {
return entry['translation'] as String;
}
}
// Cache miss: call the API
final translation = await _service.translate(
text,
target: target,
source: source,
);
// Store in cache
await prefs.setString(
key,
jsonEncode({
'translation': translation,
'cachedAt': DateTime.now().toIso8601String(),
}),
);
return translation;
}
/// Translate a batch of strings, using cache where available.
Future<List<String>> translateBatch(
List<String> texts, {
required String target,
String source = 'auto',
}) async {
final prefs = await SharedPreferences.getInstance();
final results = List<String?>.filled(texts.length, null);
final uncachedIndices = <int>[];
final uncachedTexts = <String>[];
// Check cache for each text
for (var i = 0; i < texts.length; i++) {
final key = _cacheKey(texts[i], target, source);
final cached = prefs.getString(key);
if (cached != null) {
final entry = jsonDecode(cached) as Map<String, dynamic>;
final cachedAt = DateTime.parse(entry['cachedAt'] as String);
if (DateTime.now().difference(cachedAt) < _cacheTtl) {
results[i] = entry['translation'] as String;
continue;
}
}
uncachedIndices.add(i);
uncachedTexts.add(texts[i]);
}
// Translate uncached texts in one batch call
if (uncachedTexts.isNotEmpty) {
final translated = await _service.translateBatch(
uncachedTexts,
target: target,
source: source,
);
for (var j = 0; j < uncachedIndices.length; j++) {
final idx = uncachedIndices[j];
results[idx] = translated[j];
// Cache the result
final key = _cacheKey(texts[idx], target, source);
await prefs.setString(
key,
jsonEncode({
'translation': translated[j],
'cachedAt': DateTime.now().toIso8601String(),
}),
);
}
}
return results.map((r) => r ?? '').toList();
}
/// Clear all cached translations
Future<void> clearCache() async {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys().where((k) => k.startsWith(_cachePrefix));
for (final key in keys) {
await prefs.remove(key);
}
}
}
Network requests can fail. A robust translation service needs to handle timeouts, rate limits, and transient errors gracefully. Here's an enhanced version with exponential backoff retry logic:
// lib/services/resilient_translation_service.dart
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'translation_service.dart';
class ResilientTranslationService extends TranslationService {
static const int _maxRetries = 3;
static const Duration _baseDelay = Duration(milliseconds: 500);
ResilientTranslationService({super.client});
Future<T> _withRetry<T>(Future<T> Function() operation) async {
var attempt = 0;
while (true) {
try {
return await operation();
} on TranslationException catch (e) {
// Don't retry auth errors
if (e.message.contains('Invalid API key')) rethrow;
attempt++;
if (attempt >= _maxRetries) rethrow;
// Exponential backoff: 500ms, 1s, 2s
final delay = _baseDelay * pow(2, attempt - 1).toInt();
await Future.delayed(delay);
} on TimeoutException {
attempt++;
if (attempt >= _maxRetries) {
throw TranslationException('Translation timed out after \$_maxRetries attempts');
}
await Future.delayed(_baseDelay * attempt);
}
}
}
@override
Future<String> translate(
String text, {
required String target,
String source = 'auto',
}) async {
return _withRetry(() => super.translate(text, target: target, source: source));
}
@override
Future<List<String>> translateBatch(
List<String> texts, {
required String target,
String source = 'auto',
}) async {
return _withRetry(() => super.translateBatch(texts, target: target, source: source));
}
}
A polished language switcher is essential for any multilingual app. Here's a reusable Flutter widget that displays a dropdown of supported languages:
// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
class Language {
final String code;
final String name;
final String flag;
const Language({required this.code, required this.name, required this.flag});
}
const supportedLanguages = [
Language(code: 'en', name: 'English', flag: '🇬🇧'),
Language(code: 'es', name: 'Español', flag: '🇪🇸'),
Language(code: 'fr', name: 'Français', flag: '🇫🇷'),
Language(code: 'de', name: 'Deutsch', flag: '🇩🇪'),
Language(code: 'ja', name: '日本語', flag: '🇯🇵'),
Language(code: 'zh', name: '中文', flag: '🇨🇳'),
Language(code: 'ko', name: '한국어', flag: '🇰🇷'),
Language(code: 'pt', name: 'Português', flag: '🇧🇷'),
Language(code: 'ar', name: 'عربي', flag: '🇸🇦'),
Language(code: 'hi', name: 'हिन्दी', flag: '🇮🇳'),
];
class LanguageSwitcher extends StatelessWidget {
final String currentLang;
final ValueChanged<String> onLanguageChanged;
const LanguageSwitcher({
super.key,
required this.currentLang,
required this.onLanguageChanged,
});
@override
Widget build(BuildContext context) {
final current = supportedLanguages.firstWhere(
(l) => l.code == currentLang,
orElse: () => supportedLanguages.first,
);
return PopupMenuButton<String>(
initialValue: currentLang,
onSelected: onLanguageChanged,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(current.flag, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 4),
Text(
current.code.toUpperCase(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Icon(Icons.arrow_drop_down),
],
),
),
itemBuilder: (context) => supportedLanguages
.map(
(lang) => PopupMenuItem<String>(
value: lang.code,
child: Row(
children: [
Text(lang.flag, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 12),
Text(lang.name),
if (lang.code == currentLang) ...[
const Spacer(),
const Icon(Icons.check, size: 16, color: Colors.blue),
],
],
),
),
)
.toList(),
);
}
}
If you're currently using the Google Translate API in your Flutter app, migrating to SocketsIO takes under 5 minutes. The API is Google Translate v2 compatible, so the request/response format is identical.
// BEFORE: Google Translate API
// Endpoint: https://translation.googleapis.com/language/translate/v2
// Cost: \$20 per million characters
// Auth: API key as query parameter
final response = await http.post(
Uri.parse('https://translation.googleapis.com/language/translate/v2?key=\$googleApiKey'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'q': text, 'target': target, 'source': source}),
);
// AFTER: SocketsIO Translation API
// Endpoint: https://api.socketsio.com/v1/translate
// Cost: \$3.50 per million characters (83% cheaper)
// Auth: Bearer token in Authorization header
// Free: 500K chars/month, no credit card
final response = await http.post(
Uri.parse('https://api.socketsio.com/v1/translate'),
headers: {
'Authorization': 'Bearer \$socketsioApiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({'q': text, 'target': target, 'source': source}),
);
// Response format is identical:
// {"data": {"translations": [{"translatedText": "..."}]}}
✅ Drop-in replacement: The JSON request and response format is identical to Google Translate v2. Only the endpoint URL and auth header change. Your existing response parsing code works without modification.
Cost matters at scale. Here's how SocketsIO compares to other translation APIs for Flutter apps:
| Provider | Free Tier | Cost per 1M chars | Credit Card Required | Google v2 Compatible | Languages |
|---|---|---|---|---|---|
| Google Cloud Translate | 500K chars | $20.00 | Yes | ✓ (native) | 135+ |
| DeepL API | 500K chars | $25.00 | Yes | No | 31 |
| Azure Translator | 2M chars | $10.00 | Yes | No | 100+ |
| AWS Translate | 2M chars (12 mo) | $15.00 | Yes | No | 75+ |
| SocketsIO | 500K chars/mo | $3.50 | No | ✓ Yes | 195 |
For a typical Flutter app translating 5 million characters per month (e.g., a mid-size social app with user-generated content), the monthly cost difference is stark:
Before shipping your multilingual Flutter app, keep these best practices in mind:
// Bad: API key in source code
const apiKey = 'sk-live-abc123...';
// Good: Use --dart-define or a .env approach
// Run: flutter run --dart-define=SOCKETSIO_KEY=sk-live-abc123
const apiKey = String.fromEnvironment('SOCKETSIO_KEY');
// Bad: translating in build()
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: translationService.translate(text, target: lang), // Called on every rebuild!
builder: (context, snapshot) => Text(snapshot.data ?? text),
);
}
// Good: translate once, store in state
@override
void initState() {
super.initState();
_loadTranslations();
}
Future<void> _loadTranslations() async {
final translated = await translationService.translateBatch(uiStrings, target: lang);
setState(() => _translatedStrings = translated);
}
// Wrap your app with Directionality for RTL languages
final isRtl = ['ar', 'he', 'fa', 'ur'].contains(currentLang);
Directionality(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
child: YourWidget(),
)
Translation API calls take 50-200ms. Always show a loading indicator or use skeleton screens while translations load. Never show untranslated text that then "pops" to translated text — it creates a jarring UX.
500,000 characters/month free. No credit card required. 195 languages. Google Translate v2 compatible. Production-ready with 99.9% uptime SLA.
Get Your Free API Key →Or test instantly in the API Playground — no signup needed