Internationalization Patterns with GetX
Complete guide to implementing multi-language support in Flutter applications using GetX's powerful internationalization system.
Setup
Define Translations
Create a translations class extending Translations :
// lib/core/i18n/app_translations.dart import 'package:get/get.dart';
class AppTranslations extends Translations { @override Map<String, Map<String, String>> get keys => { 'en_US': EnUS.keys, 'ar_SA': ArSA.keys, 'fr_FR': FrFR.keys, 'es_ES': EsES.keys, }; }
// English translations class EnUS { static const keys = { // Common 'app_name': 'My App', 'common_save': 'Save', 'common_cancel': 'Cancel', 'common_delete': 'Delete', 'common_edit': 'Edit', 'common_loading': 'Loading...', 'common_error': 'An error occurred',
// Authentication
'auth_login_title': 'Login',
'auth_login_email': 'Email',
'auth_login_password': 'Password',
'auth_login_submit': 'Sign In',
'auth_login_forgot_password': 'Forgot Password?',
'auth_logout': 'Logout',
// Validation
'validation_email_required': 'Email is required',
'validation_email_invalid': 'Please enter a valid email',
'validation_password_required': 'Password is required',
'validation_password_min_length': 'Password must be at least @length characters',
// Errors
'error_network': 'Network error. Please check your connection.',
'error_server': 'Server error. Please try again later.',
'error_unauthorized': 'Invalid email or password',
'error_not_found': 'Resource not found',
// Profile
'profile_title': 'Profile',
'profile_edit': 'Edit Profile',
'profile_settings': 'Settings',
'profile_logout': 'Logout',
// Settings
'settings_title': 'Settings',
'settings_language': 'Language',
'settings_theme': 'Theme',
'settings_notifications': 'Notifications',
// Pluralization
'items_count_zero': 'No items',
'items_count_one': 'One item',
'items_count_other': '@count items',
}; }
// Arabic translations class ArSA { static const keys = { // Common 'app_name': 'تطبيقي', 'common_save': 'حفظ', 'common_cancel': 'إلغاء', 'common_delete': 'حذف', 'common_edit': 'تعديل', 'common_loading': 'جار التحميل...', 'common_error': 'حدث خطأ',
// Authentication
'auth_login_title': 'تسجيل الدخول',
'auth_login_email': 'البريد الإلكتروني',
'auth_login_password': 'كلمة المرور',
'auth_login_submit': 'تسجيل الدخول',
'auth_login_forgot_password': 'نسيت كلمة المرور؟',
'auth_logout': 'تسجيل الخروج',
// Validation
'validation_email_required': 'البريد الإلكتروني مطلوب',
'validation_email_invalid': 'يرجى إدخال بريد إلكتروني صحيح',
'validation_password_required': 'كلمة المرور مطلوبة',
'validation_password_min_length': 'يجب أن تكون كلمة المرور @length أحرف على الأقل',
// Errors
'error_network': 'خطأ في الشبكة. يرجى التحقق من الاتصال.',
'error_server': 'خطأ في الخادم. يرجى المحاولة لاحقاً.',
'error_unauthorized': 'البريد الإلكتروني أو كلمة المرور غير صحيحة',
'error_not_found': 'المورد غير موجود',
// Profile
'profile_title': 'الملف الشخصي',
'profile_edit': 'تعديل الملف الشخصي',
'profile_settings': 'الإعدادات',
'profile_logout': 'تسجيل الخروج',
// Settings
'settings_title': 'الإعدادات',
'settings_language': 'اللغة',
'settings_theme': 'المظهر',
'settings_notifications': 'الإشعارات',
// Pluralization
'items_count_zero': 'لا توجد عناصر',
'items_count_one': 'عنصر واحد',
'items_count_other': '@count عنصر',
}; }
Configure GetMaterialApp
// lib/main.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'core/i18n/app_translations.dart';
void main() { runApp(const MyApp()); }
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', translations: AppTranslations(), locale: Get.deviceLocale, // Use device locale fallbackLocale: const Locale('en', 'US'), // Fallback to English initialRoute: AppRoutes.home, getPages: AppPages.pages, ); } }
Usage in Widgets
Basic Translation
// Using .tr extension Text('auth_login_title'.tr) // Displays "Login" in English, "تسجيل الدخول" in Arabic
// Alternative syntax Text(Get.find<AppLocalizations>().translate('auth_login_title'))
Translation with Parameters
// Translation key with placeholder 'validation_password_min_length': 'Password must be at least @length characters'
// Usage with parameter Text('validation_password_min_length'.trParams({'length': '8'})) // Displays: "Password must be at least 8 characters"
Pluralization
// Define plural forms in translations 'items_count_zero': 'No items', 'items_count_one': 'One item', 'items_count_other': '@count items',
// Usage String getItemsCountText(int count) { if (count == 0) { return 'items_count_zero'.tr; } else if (count == 1) { return 'items_count_one'.tr; } else { return 'items_count_other'.trParams({'count': count.toString()}); } }
// Or use a helper extension PluralExtension on String { String trPlural(int count) { final key = this; if (count == 0) { return '${key}_zero'.tr; } else if (count == 1) { return '${key}_one'.tr; } else { return '${key}_other'.trParams({'count': count.toString()}); } } }
// Usage Text('items_count'.trPlural(items.length))
Locale Management
Change Locale at Runtime
class SettingsController extends GetxController { final currentLocale = const Locale('en', 'US').obs;
void changeLanguage(String languageCode, String countryCode) { final locale = Locale(languageCode, countryCode); currentLocale.value = locale; Get.updateLocale(locale);
// Optionally save to local storage
final storage = Get.find<GetStorage>();
storage.write('locale', '$languageCode\_$countryCode');
} }
// Usage in UI DropdownButton<String>( value: controller.currentLocale.value.languageCode, items: const [ DropdownMenuItem(value: 'en', child: Text('English')), DropdownMenuItem(value: 'ar', child: Text('العربية')), DropdownMenuItem(value: 'fr', child: Text('Français')), DropdownMenuItem(value: 'es', child: Text('Español')), ], onChanged: (languageCode) { String countryCode = _getCountryCode(languageCode!); controller.changeLanguage(languageCode, countryCode); }, )
Persist Locale Preference
class LocaleService { final GetStorage _storage;
LocaleService(this._storage);
/// Get saved locale or device locale Locale getLocale() { final savedLocale = storage.read<String>('locale'); if (savedLocale != null) { final parts = savedLocale.split(''); return Locale(parts[0], parts.length > 1 ? parts[1] : ''); } return Get.deviceLocale ?? const Locale('en', 'US'); }
/// Save locale preference void saveLocale(Locale locale) { storage.write('locale', '${locale.languageCode}${locale.countryCode}'); } }
// In main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); await GetStorage.init();
final localeService = LocaleService(GetStorage()); final savedLocale = localeService.getLocale();
runApp(MyApp(initialLocale: savedLocale)); }
RTL Support
Detect RTL Languages
bool isRTL(Locale locale) { final rtlLanguages = ['ar', 'he', 'fa', 'ur']; return rtlLanguages.contains(locale.languageCode); }
Configure Text Direction
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', translations: AppTranslations(), locale: Get.deviceLocale, fallbackLocale: const Locale('en', 'US'), // Automatically set text direction based on locale builder: (context, child) { return Directionality( textDirection: isRTL(Get.locale!) ? TextDirection.rtl : TextDirection.ltr, child: child!, ); }, initialRoute: AppRoutes.home, getPages: AppPages.pages, ); } }
RTL-Aware Widgets
// Use EdgeInsetsDirectional instead of EdgeInsets Padding( padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), child: Text('Hello'), )
// Use AlignmentDirectional instead of Alignment Align( alignment: AlignmentDirectional.centerStart, // Start instead of left child: Text('Aligned Text'), )
// Use leading/trailing instead of left/right in Row Row( children: [ const Icon(Icons.arrow_forward), // Will flip in RTL const SizedBox(width: 8), Text('Next'), ], )
Date and Number Formatting
Date Formatting
import 'package:intl/intl.dart';
class DateFormatter { static String formatDate(DateTime date, Locale locale) { final formatter = DateFormat.yMMMd(locale.toString()); return formatter.format(date); }
static String formatTime(DateTime time, Locale locale) { final formatter = DateFormat.jm(locale.toString()); return formatter.format(time); }
static String formatDateTime(DateTime dateTime, Locale locale) { final formatter = DateFormat.yMMMd(locale.toString()).add_jm(); return formatter.format(dateTime); } }
// Usage final formattedDate = DateFormatter.formatDate(DateTime.now(), Get.locale!); // English: "Jan 15, 2024" // Arabic: "١٥/٠١/٢٠٢٤"
Number Formatting
import 'package:intl/intl.dart';
class NumberFormatter { static String formatNumber(num value, Locale locale) { final formatter = NumberFormat.decimalPattern(locale.toString()); return formatter.format(value); }
static String formatCurrency(num value, Locale locale, String currencySymbol) { final formatter = NumberFormat.currency( locale: locale.toString(), symbol: currencySymbol, ); return formatter.format(value); }
static String formatPercent(num value, Locale locale) { final formatter = NumberFormat.percentPattern(locale.toString()); return formatter.format(value); } }
// Usage final price = NumberFormatter.formatCurrency(99.99, Get.locale!, '$'); // English: "$99.99" // French: "99,99 $"
Translation Key Organization
Naming Convention
[feature].[screen].[widget].[state]
Examples:
- auth.login.email.label
- auth.login.email.hint
- auth.login.email.error.required
- auth.login.email.error.invalid
- profile.settings.language.title
- profile.settings.language.description
- common.button.save
- common.button.cancel
- common.error.network
- common.error.server
Hierarchical Structure
class TranslationKeys { // Common static const commonSave = 'common.button.save'; static const commonCancel = 'common.button.cancel'; static const commonLoading = 'common.loading';
// Authentication static const authLoginTitle = 'auth.login.title'; static const authLoginEmail = 'auth.login.email.label'; static const authLoginEmailHint = 'auth.login.email.hint'; static const authLoginEmailRequired = 'auth.login.email.error.required';
// Profile static const profileTitle = 'profile.title'; static const profileEdit = 'profile.edit'; }
// Usage Text(TranslationKeys.authLoginTitle.tr)
Testing Translations
Test Translation Keys
void main() { test('all translation keys exist in all languages', () { final translations = AppTranslations(); final languages = translations.keys.keys.toList();
// Get keys from first language
final referenceKeys = translations.keys[languages.first]!.keys.toSet();
// Check all other languages have same keys
for (final lang in languages.skip(1)) {
final langKeys = translations.keys[lang]!.keys.toSet();
// Missing keys
final missingKeys = referenceKeys.difference(langKeys);
expect(missingKeys, isEmpty,
reason: 'Language $lang is missing keys: $missingKeys');
// Extra keys
final extraKeys = langKeys.difference(referenceKeys);
expect(extraKeys, isEmpty,
reason: 'Language $lang has extra keys: $extraKeys');
}
}); }
Best Practices
Translation Keys:
-
Use hierarchical dot notation
-
Keep keys descriptive and consistent
-
Avoid hardcoded strings in UI code
-
Define constants for frequently used keys
Locale Management:
-
Persist user's locale preference
-
Fallback to device locale when appropriate
-
Provide locale switching in settings
-
Test with different locales
RTL Support:
-
Use directional classes (EdgeInsetsDirectional, AlignmentDirectional)
-
Test UI with RTL languages (Arabic, Hebrew)
-
Flip icons and layouts appropriately
-
Consider RTL implications in custom widgets
Formatting:
-
Use intl package for dates and numbers
-
Format currency with correct symbols and positions
-
Handle pluralization correctly
-
Consider cultural differences (date formats, number separators)
Performance:
-
Load only needed translations
-
Cache formatted strings when appropriate
-
Avoid translating in build methods unnecessarily
-
Use const where possible
Maintenance:
-
Keep translations in sync across languages
-
Use translation management tools for large projects
-
Involve native speakers for quality translations
-
Test translations with real content (not Lorem Ipsum)