← Back to Blog

Flutter GetX Localization: Complete Guide to Internationalization with GetX

fluttergetxlocalizationi18nstate-managementtranslations

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'.tr translation 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 .tr anywhere 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.