Flutter GetX Localization: Complete Guide to Internationalization with GetX
GetX is one of Flutter's most popular state management solutions, and it comes with powerful built-in localization support. This comprehensive guide shows you how to implement multi-language support in Flutter using GetX.
Why Use GetX for Localization?
GetX offers several advantages for Flutter localization:
- No BuildContext required - Access translations anywhere
- Dynamic locale switching - Change languages at runtime without rebuilding
- Simple syntax -
'key'.trtranslation pattern - Parameter support - Easy placeholder handling
- Pluralization - Built-in plural support
- Lightweight - No code generation required
Getting Started
Installation
Add GetX to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
get: ^4.6.6
Basic Setup
Create your translation files:
// lib/translations/app_translations.dart
import 'package:get/get.dart';
class AppTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': EnUs.translations,
'de_DE': DeDe.translations,
'fr_FR': FrFr.translations,
'es_ES': EsEs.translations,
'ar_SA': ArSa.translations,
};
}
Language Files
Create separate files for each language:
// lib/translations/en_us.dart
class EnUs {
static const Map<String, String> translations = {
// General
'app_name': 'My Flutter App',
'welcome': 'Welcome',
'hello': 'Hello, @name!',
// Authentication
'login': 'Login',
'logout': 'Logout',
'register': 'Register',
'email': 'Email',
'password': 'Password',
'forgot_password': 'Forgot Password?',
// Navigation
'home': 'Home',
'profile': 'Profile',
'settings': 'Settings',
// Actions
'save': 'Save',
'cancel': 'Cancel',
'delete': 'Delete',
'edit': 'Edit',
'confirm': 'Confirm',
// Messages
'success': 'Success!',
'error': 'An error occurred',
'loading': 'Loading...',
'no_data': 'No data available',
// Plurals
'items_count': '@count item(s)',
'messages_count': '@count message(s)',
};
}
// lib/translations/de_de.dart
class DeDe {
static const Map<String, String> translations = {
// General
'app_name': 'Meine Flutter App',
'welcome': 'Willkommen',
'hello': 'Hallo, @name!',
// Authentication
'login': 'Anmelden',
'logout': 'Abmelden',
'register': 'Registrieren',
'email': 'E-Mail',
'password': 'Passwort',
'forgot_password': 'Passwort vergessen?',
// Navigation
'home': 'Startseite',
'profile': 'Profil',
'settings': 'Einstellungen',
// Actions
'save': 'Speichern',
'cancel': 'Abbrechen',
'delete': 'Löschen',
'edit': 'Bearbeiten',
'confirm': 'Bestätigen',
// Messages
'success': 'Erfolg!',
'error': 'Ein Fehler ist aufgetreten',
'loading': 'Wird geladen...',
'no_data': 'Keine Daten verfügbar',
// Plurals
'items_count': '@count Artikel',
'messages_count': '@count Nachricht(en)',
};
}
// lib/translations/fr_fr.dart
class FrFr {
static const Map<String, String> translations = {
'app_name': 'Mon Application Flutter',
'welcome': 'Bienvenue',
'hello': 'Bonjour, @name!',
'login': 'Connexion',
'logout': 'Déconnexion',
'register': 'Inscription',
'email': 'E-mail',
'password': 'Mot de passe',
'forgot_password': 'Mot de passe oublié?',
'home': 'Accueil',
'profile': 'Profil',
'settings': 'Paramètres',
'save': 'Enregistrer',
'cancel': 'Annuler',
'delete': 'Supprimer',
'edit': 'Modifier',
'confirm': 'Confirmer',
'success': 'Succès!',
'error': 'Une erreur est survenue',
'loading': 'Chargement...',
'no_data': 'Aucune donnée disponible',
'items_count': '@count article(s)',
'messages_count': '@count message(s)',
};
}
Configure GetMaterialApp
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'translations/app_translations.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'app_name'.tr,
translations: AppTranslations(),
locale: Get.deviceLocale, // Use device locale
fallbackLocale: const Locale('en', 'US'), // Fallback if locale not found
supportedLocales: const [
Locale('en', 'US'),
Locale('de', 'DE'),
Locale('fr', 'FR'),
Locale('es', 'ES'),
Locale('ar', 'SA'),
],
home: HomePage(),
);
}
}
Using Translations
Basic Translation
// Simple translation
Text('welcome'.tr) // "Welcome" in English
// In any widget
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('login'.tr),
Text('email'.tr),
Text('password'.tr),
ElevatedButton(
onPressed: () {},
child: Text('login'.tr),
),
],
);
}
}
Translations with Parameters
// Define in translation file:
'hello': 'Hello, @name!'
'order_status': 'Your order #@orderId is @status'
// Use with parameters:
Text('hello'.trParams({'name': 'John'})) // "Hello, John!"
Text('order_status'.trParams({
'orderId': '12345',
'status': 'delivered'.tr,
})) // "Your order #12345 is Delivered"
Translation Outside Widgets
One of GetX's biggest advantages - access translations anywhere:
// In a service
class AuthService extends GetxService {
void login() async {
try {
await _doLogin();
Get.snackbar('success'.tr, 'login_success'.tr);
} catch (e) {
Get.snackbar('error'.tr, 'login_failed'.tr);
}
}
}
// In a controller
class HomeController extends GetxController {
String get welcomeMessage => 'hello'.trParams({'name': userName});
void showError() {
Get.dialog(
AlertDialog(
title: Text('error'.tr),
content: Text('error_message'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('confirm'.tr),
),
],
),
);
}
}
// In a utility function
String formatPrice(double price) {
return '${'price'.tr}: \$${price.toStringAsFixed(2)}';
}
Changing Locale at Runtime
Simple Locale Switch
// Change to German
Get.updateLocale(const Locale('de', 'DE'));
// Change to French
Get.updateLocale(const Locale('fr', 'FR'));
// Get current locale
print(Get.locale); // Locale('en', 'US')
Language Selector Widget
class LanguageSelector extends StatelessWidget {
final List<LanguageOption> languages = [
LanguageOption('English', 'en', 'US', '🇺🇸'),
LanguageOption('Deutsch', 'de', 'DE', '🇩🇪'),
LanguageOption('Français', 'fr', 'FR', '🇫🇷'),
LanguageOption('Español', 'es', 'ES', '🇪🇸'),
LanguageOption('العربية', 'ar', 'SA', '🇸🇦'),
];
@override
Widget build(BuildContext context) {
return PopupMenuButton<LanguageOption>(
onSelected: (language) {
Get.updateLocale(Locale(language.languageCode, language.countryCode));
},
itemBuilder: (context) => languages.map((lang) {
final isSelected = Get.locale?.languageCode == lang.languageCode;
return PopupMenuItem(
value: lang,
child: Row(
children: [
Text(lang.flag, style: const TextStyle(fontSize: 24)),
const SizedBox(width: 12),
Text(
lang.name,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
if (isSelected) ...[
const Spacer(),
const Icon(Icons.check, color: Colors.green),
],
],
),
);
}).toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
languages.firstWhere(
(l) => l.languageCode == Get.locale?.languageCode,
orElse: () => languages.first,
).flag,
style: const TextStyle(fontSize: 24),
),
const Icon(Icons.arrow_drop_down),
],
),
);
}
}
class LanguageOption {
final String name;
final String languageCode;
final String countryCode;
final String flag;
LanguageOption(this.name, this.languageCode, this.countryCode, this.flag);
}
Persisting Language Selection
import 'package:get_storage/get_storage.dart';
class LocaleService extends GetxService {
final _storage = GetStorage();
static const _localeKey = 'selected_locale';
Future<LocaleService> init() async {
await GetStorage.init();
return this;
}
Locale? getSavedLocale() {
final saved = _storage.read<String>(_localeKey);
if (saved != null) {
final parts = saved.split('_');
return Locale(parts[0], parts.length > 1 ? parts[1] : '');
}
return null;
}
Future<void> saveLocale(Locale locale) async {
await _storage.write(
_localeKey,
'${locale.languageCode}_${locale.countryCode}',
);
}
Future<void> updateLocale(Locale locale) async {
await saveLocale(locale);
Get.updateLocale(locale);
}
}
// In main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final localeService = await Get.putAsync(() => LocaleService().init());
final savedLocale = localeService.getSavedLocale();
runApp(MyApp(initialLocale: savedLocale));
}
class MyApp extends StatelessWidget {
final Locale? initialLocale;
const MyApp({this.initialLocale});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
translations: AppTranslations(),
locale: initialLocale ?? Get.deviceLocale,
fallbackLocale: const Locale('en', 'US'),
// ...
);
}
}
Advanced Pluralization
Manual Plural Handling
// translations
'cart_items_zero': 'Your cart is empty',
'cart_items_one': '1 item in your cart',
'cart_items_other': '@count items in your cart',
// Usage
String getCartText(int count) {
if (count == 0) return 'cart_items_zero'.tr;
if (count == 1) return 'cart_items_one'.tr;
return 'cart_items_other'.trParams({'count': count.toString()});
}
Custom Plural Extension
extension PluralTranslation on String {
String plural(int count, {Map<String, String>? params}) {
String key;
if (count == 0) {
key = '${this}_zero';
} else if (count == 1) {
key = '${this}_one';
} else if (count >= 2 && count <= 4) {
key = '${this}_few';
} else {
key = '${this}_other';
}
// Check if specific plural form exists, fallback to other
final translation = key.tr;
if (translation == key) {
key = '${this}_other';
}
final allParams = {'count': count.toString(), ...?params};
return key.trParams(allParams);
}
}
// Usage
Text('cart_items'.plural(itemCount))
RTL Support with GetX
Automatic Direction
// lib/translations/ar_sa.dart
class ArSa {
static const Map<String, String> translations = {
'app_name': 'تطبيقي',
'welcome': 'مرحبا',
'hello': 'مرحبا، @name!',
'login': 'تسجيل الدخول',
'logout': 'تسجيل الخروج',
'settings': 'الإعدادات',
'home': 'الرئيسية',
'profile': 'الملف الشخصي',
};
}
RTL-Aware Widget
class DirectionalWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isRtl = Get.locale?.languageCode == 'ar' ||
Get.locale?.languageCode == 'he' ||
Get.locale?.languageCode == 'fa';
return Directionality(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
child: Scaffold(
appBar: AppBar(
title: Text('settings'.tr),
),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.language),
title: Text('language'.tr),
trailing: const Icon(Icons.arrow_forward_ios),
),
],
),
),
);
}
}
Organizing Large Translation Files
Modular Structure
lib/
translations/
app_translations.dart
en/
common.dart
auth.dart
home.dart
settings.dart
de/
common.dart
auth.dart
home.dart
settings.dart
// lib/translations/en/common.dart
class EnCommon {
static const Map<String, String> translations = {
'save': 'Save',
'cancel': 'Cancel',
'delete': 'Delete',
'edit': 'Edit',
'loading': 'Loading...',
'error': 'Error',
'success': 'Success',
};
}
// lib/translations/en/auth.dart
class EnAuth {
static const Map<String, String> translations = {
'auth.login': 'Login',
'auth.logout': 'Logout',
'auth.register': 'Register',
'auth.email': 'Email',
'auth.password': 'Password',
'auth.forgot_password': 'Forgot Password?',
'auth.login_success': 'Successfully logged in',
'auth.login_failed': 'Login failed. Please try again.',
};
}
// Merge all translations
class EnUs {
static Map<String, String> get translations => {
...EnCommon.translations,
...EnAuth.translations,
...EnHome.translations,
...EnSettings.translations,
};
}
Testing GetX Localization
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
void main() {
setUp(() {
Get.testMode = true;
});
tearDown(() {
Get.reset();
});
group('Translations', () {
test('English translations work', () {
Get.addTranslations({
'en_US': {
'hello': 'Hello',
'greeting': 'Hello, @name!',
},
});
Get.locale = const Locale('en', 'US');
expect('hello'.tr, 'Hello');
expect('greeting'.trParams({'name': 'John'}), 'Hello, John!');
});
test('German translations work', () {
Get.addTranslations({
'de_DE': {
'hello': 'Hallo',
'greeting': 'Hallo, @name!',
},
});
Get.locale = const Locale('de', 'DE');
expect('hello'.tr, 'Hallo');
expect('greeting'.trParams({'name': 'John'}), 'Hallo, John!');
});
test('Fallback works for missing translation', () {
Get.addTranslations({
'en_US': {'hello': 'Hello'},
});
Get.locale = const Locale('fr', 'FR');
Get.fallbackLocale = const Locale('en', 'US');
expect('hello'.tr, 'Hello'); // Falls back to English
});
});
testWidgets('Widget displays correct translation', (tester) async {
Get.addTranslations({
'en_US': {'welcome': 'Welcome'},
});
await tester.pumpWidget(
GetMaterialApp(
locale: const Locale('en', 'US'),
home: Scaffold(body: Text('welcome'.tr)),
),
);
expect(find.text('Welcome'), findsOneWidget);
});
}
GetX vs Official Flutter Localization
| Feature | GetX | Official (gen-l10n) |
|---|---|---|
| Setup Complexity | Simple | Moderate |
| Code Generation | No | Yes |
| Context Required | No | Yes |
| Type Safety | No | Yes |
| IDE Support | Basic | Full |
| Bundle Size | Larger | Smaller |
| Flexibility | High | Moderate |
When to Use GetX Localization
- Already using GetX for state management
- Need translations outside widget tree
- Want simple setup without code generation
- Building smaller apps
When to Use Official Localization
- Need type-safe translations
- Want IDE autocomplete for keys
- Building larger, enterprise apps
- Following Google's recommended approach
Best Practices
1. Use Consistent Key Naming
// Good
'auth.login_button'
'auth.login_success'
'auth.login_error'
// Bad
'loginBtn'
'login_success_message'
'errorLogin'
2. Avoid String Concatenation
// Bad
Text('${'hello'.tr} ${'world'.tr}')
// Good - Define as single key
'hello_world': 'Hello World'
Text('hello_world'.tr)
3. Handle Missing Translations
extension SafeTranslation on String {
String get trSafe {
final translated = tr;
if (translated == this) {
// Log missing translation in debug mode
debugPrint('Missing translation for key: $this');
}
return translated;
}
}
4. Create Translation Keys Constants
abstract class TrKeys {
static const login = 'auth.login';
static const logout = 'auth.logout';
static const email = 'auth.email';
static const password = 'auth.password';
}
// Usage
Text(TrKeys.login.tr)
Conclusion
GetX provides a powerful and flexible localization solution for Flutter apps. Its ability to access translations without BuildContext makes it particularly useful for complex applications with services and controllers that need localized strings.
Key takeaways:
- Simple setup - No code generation required
- Flexible access - Use
.tranywhere in your code - Runtime switching - Change locale without rebuilding
- Parameter support - Easy dynamic content
- Test friendly - Simple to test translations
For teams already using GetX, its localization features integrate seamlessly. For those needing more type safety and IDE support, consider combining GetX with Flutter's official localization or using FlutterLocalisation for the best of both worlds - easy management with type-safe generated code.