Flutter Multi Language Tutorial Example | Localization

Getx offers many convenient features for state managment. It also features about Localization or multiple language support.

It has great support for loading json data for multiple language app. We would be using locale, translations and fallbacklocale properties. 

The above three properties helps to realize building multiple language app using Getx.

We will cover the below things here

  • 1. Flutter locale
  • 2. Flutter default locale and fallbacklocale
  • 3. Flutter internationalization
  • 4. Controller for localization
  • 5. load json data 
  • 6. save them in sharedPreference

JSON Files

Make sure that you have json files for language information in your assets folder. Here's en.json

{
  "select_language": "Select Language",
  "save": "Save",
  "select_a_language": "Select a language",
  "home": "Home",
  "back_to_home": "back to home"
}

And where's bn.json

{
  "select_language": "একটি ভাষা নির্বাচন করুন",
  "save": "সংরক্ষণ",
  "select_a_language": "একটি ভাষা নির্বাচন করুন",
  "home": "বাড়ি",
  "back_to_home": "বাড়িতে ফিরে যাও"
}

You can use any kind of language, just make sure your json files may correct format and translation.

App Constants

Let's define our app constants file. We will need for storing data to sharedPreferences. Our loaded JSON files would be stored in sharedPreference

class AppConstants {
  /*
  Localization data
   */
  static const String COUNTRY_CODE = 'country_code';
  static const String LANGUAGE_CODE = 'language_code';
  
  static List<LanguageModel> languages = [
    LanguageModel(imageUrl: "xx", languageName: 'English', countryCode: 'US', languageCode: 'en'),
    LanguageModel(imageUrl: "xx", languageName: 'বাংলা', countryCode: 'BD', languageCode: 'bn'),
  ];
}

Here we have String COUNTRY_CODE and LANGUAGE_CODE for storing data. Here we also instantiate two objects for our languages using LanguageModel.

At the same we return them in a List so that we can access them later from dependency injection file.

Language Model

Let's first create Language model class. It will save 

  1. country flag url
  2. country code
  3. country language code
  4. language name
class LanguageModel {
  String imageUrl;
  String languageName;
  String languageCode;
  String countryCode;

  LanguageModel({
    required this.imageUrl,
    required this.languageName,
    required this.countryCode,
    required this.languageCode});
}

Getx Dependencies

Next let's create a class load our dependencies. This dependency file will load Localization Controller, SharedPreference and JSON data from local device.

Future<Map<String, Map<String, String>>> init() async {

  final sharedPreference = await SharedPreferences.getInstance();
  Get.lazyPut(() => sharedPreference);

  Get.lazyPut(() => LocalizationController(sharedPreferences: Get.find()));


  // Retrieving localized data
  Map<String, Map<String, String>> _languages = Map();
  for(LanguageModel languageModel in AppConstants.languages) {
    String jsonStringValues =  await rootBundle.loadString('assets/language/${languageModel.languageCode}.json');
    Map<String, dynamic> _mappedJson = json.decode(jsonStringValues);
    Map<String, String> _json = Map();

    _mappedJson.forEach((key, value) {

      _json[key] = value.toString();
    });
    _languages['${languageModel.languageCode}_${languageModel.countryCode}'] = _json;
  }

  return _languages;
}

Here we access the List that we created before in App Constants file. We acess them AppConstants.languages inside for loop.

Localization Controller

Let's create actual controller. This controller be responsible for doing the below things

  1.  set a default language for the app
  2. Load current lanauge from device is there's any
  3.  set language based on user's click
  4.  save the choosen language
class LocalizationController extends GetxController implements GetxService {
  final SharedPreferences sharedPreferences;

  LocalizationController({required this.sharedPreferences}) {
    loadCurrentLanguage();
  }

  Locale _locale = Locale(AppConstants.languages[0].languageCode,
      AppConstants.languages[0].countryCode);
  bool _isLtr = true;
  List<LanguageModel> _languages = [];

  Locale get locale => _locale;

  bool get isLtr => _isLtr;

  List<LanguageModel> get languages => _languages;

  void setLanguage(Locale locale) {
    Get.updateLocale(locale);
    _locale = locale;
    if (_locale.languageCode == 'bn') {
      _isLtr = false;
    } else {
      _isLtr = true;
    }
    saveLanguage(_locale);
    update();
  }

  void loadCurrentLanguage() async {
    _locale = Locale(sharedPreferences.getString(AppConstants.LANGUAGE_CODE) ??
        AppConstants.languages[0].languageCode,
        sharedPreferences.getString(AppConstants.COUNTRY_CODE) ??
            AppConstants.languages[0].countryCode);
    _isLtr = _locale.languageCode != 'bn';
    for (int index = 0; index < AppConstants.languages.length; index++) {
      if (AppConstants.languages[index].languageCode == _locale.languageCode) {
        _selectedIndex = index;
        break;
      }
    }
    _languages = [];
    _languages.addAll(AppConstants.languages);
    update();
  }

  void saveLanguage(Locale locale) async {
    sharedPreferences.setString(
        AppConstants.LANGUAGE_CODE, locale.languageCode);
    sharedPreferences.setString(AppConstants.COUNTRY_CODE, locale.countryCode!);
  }

  int _selectedIndex = 0;

  int get selectedIndex => _selectedIndex;

  void setSelectIndex(int index) {
    _selectedIndex = index;
    update();
  }

  void searchLanguage(String query) {
    if (query.isEmpty) {
      _languages = [];
      _languages = AppConstants.languages;
    } else {
      _selectedIndex = -1;
      _languages = [];
      AppConstants.languages.forEach((language) async {
        if (language.languageName.toLowerCase().contains(query.toLowerCase())) {
          _languages.add(language);
        }
      });
    }
    update();
  }
}

In the LocalizationController we have a function

  void loadCurrentLanguage() async {
    _locale = Locale(sharedPreferences.getString(AppConstants.LANGUAGE_CODE) ??
        AppConstants.languages[0].languageCode,
        sharedPreferences.getString(AppConstants.COUNTRY_CODE) ??
            AppConstants.languages[0].countryCode);
    _isLtr = _locale.languageCode != 'bn';
    for (int index = 0; index < AppConstants.languages.length; index++) {
      if (AppConstants.languages[index].languageCode == _locale.languageCode) {
        _selectedIndex = index;
        break;
      }
    }
    _languages = [];
    _languages.addAll(AppConstants.languages);
    update();
  }

The above function, loads if we have any saved language in the app using locale(). Locale takes language code and country code.

 

We will also need a route file. let's define our routes.

class RouteHelper {
  static const String initial = '/';
  static const String splash = '/splash';
  static const String language='/language';
  static String getSplashRoute() => '$splash';
  static String getInitialRoute()=>'$initial';
  static String getLanguageRoute()=>'$language';

  static List<GetPage> routes = [

    GetPage(name: splash, page: () {
      return SplashScreen();
    }),
    GetPage(name: initial, page:(){
      return HomePage();
    }),
    GetPage(name:language, page:(){
      return LanguagePage();
    })
  ];
}

After splash screen user will go to LanguagePage(). On Languge screen user will be able to select a language and see the changes immediately.

Let's create our splash screen

class SplashScreen extends StatefulWidget {
  const SplashScreen({Key? key}) : super(key: key);

  @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMixin{
  late Animation<double> animation;
  late AnimationController _controller;
  GlobalKey<ScaffoldState> _globalKey = GlobalKey();

  @override
  dispose() {
    _controller.dispose();
    super.dispose();
  }

  void initState() {
    super.initState();
    _controller =
    new AnimationController(vsync: this, duration: Duration(seconds: 2))..forward()
    ;
    animation = new CurvedAnimation(parent: _controller,
        curve: Curves.linear);
    Timer(
        Duration(seconds: 3),()=>Get.offNamed(RouteHelper.getLanguageRoute())
    );
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _globalKey,
      backgroundColor: Colors.white,
      body: Column(
        //crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ScaleTransition(
              scale: animation,
              child: Center(child: Image.asset("img/logo part 1.png", width:200))),
          Center(child: Image.asset("img/logo part 2.png", width:200,)),
        ],
      ),
    );
  }
}

 

After that we will create language screen. Here user will select the language and we will change the localization based on the user interaction.

class LanguagePage extends StatelessWidget {
  final bool fromMenu;
  LanguagePage({this.fromMenu = false});

  @override
  Widget build(BuildContext context) {
    double? width = 375;
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: GetBuilder<LocalizationController>(builder: (localizationController) {
          return Column(children: [

            Expanded(child: Center(
              child: Scrollbar(
                child: SingleChildScrollView(
                  physics: BouncingScrollPhysics(),
                  padding: EdgeInsets.all(5),
                  child: Center(child: SizedBox(
                    width: width,
                    child: Column(mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.center, children: [

                          Center(child: Image.asset("img/logo part 1.png", width: 120)),
                          SizedBox(height: 5),
                          Center(child: Image.asset("img/logo part 2.png", width: 140)),

                          SizedBox(height: 30),

                          Padding(
                            padding: EdgeInsets.symmetric(horizontal: 10),
                            child: Text('select_language'.tr,),
                          ),
                          SizedBox(height: 10),

                          GridView.builder(
                              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                                crossAxisCount:  2,
                                childAspectRatio: 1,
                              ),
                              itemCount: 2,//localizationController.languages.length,
                              physics: NeverScrollableScrollPhysics(),
                              shrinkWrap: true,
                              itemBuilder: (context, index) => LanguageWidget(
                                languageModel: localizationController.languages[index],
                                localizationController: localizationController, index: index,
                              )
                          ),


                          SizedBox(height: 10),

                          Text('you_can_change_language'.tr, ),

                        ]),
                  )),
                ),
              ),
            )),
            ElevatedButton(
              child: Text('save'.tr),

              onPressed: () {
                if(localizationController.languages.length > 0 && localizationController.selectedIndex != -1) {
                  localizationController.setLanguage(Locale(
                    AppConstants.languages[localizationController.selectedIndex].languageCode,
                    AppConstants.languages[localizationController.selectedIndex].countryCode,
                  ));
                  if (fromMenu) {
                    Navigator.pop(context);
                  } else {
                    Get.offNamed(RouteHelper.getInitialRoute());
                  }
                }else {
                  Get.snackbar('select_a_language'.tr, 'select_a_language'.tr);

                }
              },
            ),
          ]);
        }),
      ),
    );
  }
}

 

After selecting and saving the language we will go to home page

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Get.offAllNamed(RouteHelper.getLanguageRoute());
          },
          child: Text("home".tr)

        ),
      )
    );
  }
}

Our language widget

class LanguageWidget extends StatelessWidget {
  final LanguageModel languageModel;
  final LocalizationController localizationController;
  final int index;
  LanguageWidget({required this.languageModel, required this.localizationController,
    required this.index});

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        localizationController.setLanguage(Locale(
          AppConstants.languages[index].languageCode,
          AppConstants.languages[index].countryCode,
        ));
        localizationController.setSelectIndex(index);
      },
      child: Container(
        padding: EdgeInsets.all(10),
        margin: EdgeInsets.all(5),
        decoration: BoxDecoration(
          color: Theme.of(context).cardColor,
          borderRadius: BorderRadius.circular(10),
          boxShadow: [BoxShadow(
              color: Colors.grey[200]!,
              blurRadius: 5, spreadRadius: 1)],
        ),
        child: Stack(children: [

          Center(
            child: Column(mainAxisSize: MainAxisSize.min, children: [
              SizedBox(height: 5),
              Text(languageModel.languageName, ),
            ]),
          ),

          localizationController.selectedIndex == index ? Positioned(
            top: 0, right: 0, left: 0, bottom: 40,
            child: Icon(Icons.check_circle, color: Theme.of(context).primaryColor, size: 25),
          ) : SizedBox(),

        ]),
      ),
    );
  }
}

Message class is needed for internationalization

class Messages extends Translations {
  final Map<String, Map<String, String>> languages;
  Messages({required this.languages});

  @override
  Map<String, Map<String, String>> get keys {
    return languages;
  }
}

The above class extends Translations class of Getx

Now let's take a look at main.dart

Future<void> main() async {
  // setPathUrlStrategy();
  WidgetsFlutterBinding.ensureInitialized();

  Map<String, Map<String, String>> _languages = await dep.init();

  runApp(MyApp(languages: _languages));
}

class MyApp extends StatelessWidget {
  final Map<String, Map<String, String>> languages;

  MyApp({required this.languages });
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetBuilder<LocalizationController>(builder: (localizationController){
    return GetMaterialApp(

      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        primaryColor: Colors.blue,
        fontFamily: "Lato",
      ),
      locale: localizationController.locale,
      translations: Messages(languages: languages),
      fallbackLocale: Locale(AppConstants.languages[0].languageCode,
          AppConstants.languages[0].countryCode),
      initialRoute: RouteHelper.getSplashRoute(),
      getPages: RouteHelper.routes,
      defaultTransition: Transition.topLevel,
    );
  });
  }
}

Here we have three properties local and translation and fallbacklocale. They are very important settings. 

locale: We can set this property using locationzationController.locale. It's the default locale. 

translations: This has to do with JSON file format. Here you have to mention your JSON key value pair type.

fallbacklocale: Here, we need to set up local(language) if default language is not found.

Recent posts