Sunday, March 24, 2019

Flutter - Localization or Multi Language support with Examples.

If you have created a mobile application and you want to increase users of the mobile application. So, we need to support multiple languages in our application. In mobile, with the help of localization, you can change the language of application labels and we can render the content of the app into other languages. We can customize the app for each target market and user.

We can display dates, times, and numbers in particular user readable formats. Android and iOS is the most popular mobile operating system and it runs on millions of devices in many regions. So, if we implement an app which is localized for all the regions, then it will reach most of the users. 

In this post, we are going to create a Flutter application that supports localization for English and Hindi language. The final output of example will look like below:

Basic requirements of multiple languages in Flutter. 

  • By default, the Flutter localize application will work according to Smartphone (system settings) configured language.
  • If the default language is not supported by the application, English(en) becomes the default language of the application.
  • Translations are stored in language specific JSON files in key and value format.
  • The end user can change working language from a list of supported languages.
  • When the user selects another language, the whole application layout will refresh for display selected language values.

Creating a new Project
1. Create a new project from File ⇒ New Flutter Project with your development IDE.

2. We’re going to use package flutter_localizations that is based on Dart intl package and it supports 24 languages. The Locale class is used to identity the user’s language. One of the properties of this class defines languageCode. You have to add it as a dependency to your pubspec.yaml file as follows:

as you can see above, we have created localization_en.json for English and localization_hi.json for Hindi language support in this example. We have put these files in the assets directory of the project.  It is the example of defining multiple language values files to support localization.
{ "tab_home": "Home", "key_password": "Password", "key_must_be_at_least_6_characters": "Must be at least 6 characters", "key_email": "Email", "key_please_enter_valid_email": "Please enter valid email", "key_last_name": "Last name", "key_enter_last_name": "Enter last name", "key_first_name": "First name", "key_enter_first_name": "Enter first name", "key_next": "Next" }
{ "key_password": "पारण शब्द", "key_must_be_at_least_6_characters": "कम से कम 6 अक्षर का होना चाहिए", "key_email": "ईमेल", "key_please_enter_valid_email": "कृपया वैध ईमेल दर्ज़ करें", "key_last_name": "अंतिम नाम", "key_enter_last_name": "अंतिम नाम दर्ज करो", "key_first_name": "पहला नाम", "key_enter_first_name": "पहला नाम डालो", "key_next": "आगामी" }
3. After that open main.dart file and edit it. As we have set our theme and change debug banner property of Application. In this file, we have created an instance of AppTranslationsDelegate for initializing it and pass it, in the root widget build method. 
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_localization/localization/app_translations_delegate.dart'; import 'package:flutter_localization/localization/application.dart'; import 'package:flutter_localization/signupscreen.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; Future<Null> main() async { runApp(new LocalisedApp()); } class LocalisedApp extends StatefulWidget { @override LocalisedAppState createState() { return new LocalisedAppState(); } } class LocalisedAppState extends State<LocalisedApp> { AppTranslationsDelegate _newLocaleDelegate; @override void initState() { super.initState(); _newLocaleDelegate = AppTranslationsDelegate(newLocale: null); application.onLocaleChanged = onLocaleChange; } @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: SignUpUI(), localizationsDelegates: [ _newLocaleDelegate, //provides localised strings GlobalMaterialLocalizations.delegate, //provides RTL support GlobalWidgetsLocalizations.delegate, ], supportedLocales: [ const Locale("en", ""), const Locale("es", ""), ], ); } void onLocaleChange(Locale locale) { setState(() { _newLocaleDelegate = AppTranslationsDelegate(newLocale: locale); }); } }

Now, create a class AppTranslations for fetch JSON files from the assets locale directory. The common way of doing this is creating a class that has all the localization helpers to use in the whole app.

import 'dart:async'; import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class AppTranslations { Locale locale; static Map<dynamic, dynamic> _localisedValues; AppTranslations(Locale locale) { this.locale = locale; } static AppTranslations of(BuildContext context) { return Localizations.of<AppTranslations>(context, AppTranslations); } static Future<AppTranslations> load(Locale locale) async { AppTranslations appTranslations = AppTranslations(locale); String jsonContent = await rootBundle.loadString("assets/locale/localization_${locale.languageCode}.json"); _localisedValues = json.decode(jsonContent); return appTranslations; } get currentLanguage => locale.languageCode; String text(String key) { return _localisedValues[key] ?? "$key not found"; } }
Apart from being responsible for fetch the JSON file data. This class decodes the JSON map and returns the value from the map for the corresponding key from the String text(String key) method.

This class has the main methods.
  • load function will load the string resources from the desired Locale as you can see in the above parameter.
  • of function will be a helper like any other InheritedWidget to facilitate the access to any string from any part of the app code.
5. After that, create class AppTranslationsDelegate for check locale is supported or not. It'll also reload AppTranslations class whenever the locale changes and provide AppTranslations class with the newLocale selected.
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_localization/localization/app_translations.dart'; import 'package:flutter_localization/localization/application.dart'; class AppTranslationsDelegate extends LocalizationsDelegate<AppTranslations> { final Locale newLocale; const AppTranslationsDelegate({this.newLocale}); @override bool isSupported(Locale locale) { return application.supportedLanguagesCodes.contains(locale.languageCode); } @override Future<AppTranslations> load(Locale locale) { return AppTranslations.load(newLocale ?? locale); } @override bool shouldReload(LocalizationsDelegate<AppTranslations> old) { return true; } }
  • load: the load method must method return an object that contains a collection of related resources. We'll return our AppLocalizations.load.
  • isSupported: it returns true if the app has support for the received locale.
  • shouldReload: if this method returns true then all the app widgets will be rebuilt after a load of resources.
6. Now create an Application class that declares a LocaleChangeCallback. It'll reflect localization changes everywhere in the app.
import 'dart:ui'; class Application { static final Application _application = Application._internal(); factory Application() { return _application; } Application._internal(); final List<String> supportedLanguages = [ "English", "Hindi", ]; final List<String> supportedLanguagesCodes = [ "en", "hi", ]; //returns the list of supported Locales Iterable<Locale> supportedLocales() =><Locale>((language) => Locale(language, "")); //function to be invoked when changing the language LocaleChangeCallback onLocaleChanged; } Application application = Application(); typedef void LocaleChangeCallback(Locale locale);
7. Now, let's work on user interface and use above define class in our example. In the below file, we have designed a sign-up screen and we'll implement localization on these input fields.
import 'package:flutter/material.dart'; import 'package:flutter_localization/custom_text_field.dart'; import 'package:flutter_localization/localization/app_translations.dart'; import 'package:flutter_localization/localization/application.dart'; class SignUpUI extends StatefulWidget { @override _SignUpScreenState createState() => new _SignUpScreenState(); } class _SignUpScreenState extends State<SignUpUI> { static final List<String> languagesList = application.supportedLanguages; static final List<String> languageCodesList = application.supportedLanguagesCodes; final Map<dynamic, dynamic> languagesMap = { languagesList[0]: languageCodesList[0], languagesList[1]: languageCodesList[1], }; String label = languagesList[0]; final formKey = new GlobalKey<FormState>(); final scaffoldKey = new GlobalKey<ScaffoldState>(); final teFirstName = TextEditingController(); final teLastName = TextEditingController(); final teEmail = TextEditingController(); final tePassword = TextEditingController(); FocusNode _focusNodeFirstName = new FocusNode(); FocusNode _focusNodeLastName = new FocusNode(); FocusNode _focusNodeEmail = new FocusNode(); FocusNode _focusNodePass = new FocusNode(); @override void initState() { super.initState(); application.onLocaleChanged = onLocaleChange; onLocaleChange(Locale(languagesMap["Hindi"])); } void onLocaleChange(Locale locale) async { setState(() { AppTranslations.load(locale); }); } @override void dispose() { teFirstName.dispose(); teLastName.dispose(); teEmail.dispose(); tePassword.dispose(); super.dispose(); } void _select(String language) { print("dd "+language); onLocaleChange(Locale(languagesMap[language])); setState(() { if (language == "Hindi") { label = "हिंदी"; } else { label = language; } }); } @override Widget build(BuildContext context) { var signUpForm = new Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ new Container( alignment:, margin: EdgeInsets.fromLTRB(10.0, 30.0, 10.0, 0.0), padding: EdgeInsets.fromLTRB(10.0, 50.0, 10.0, 10.0), decoration: new BoxDecoration( color: const Color.fromRGBO(255, 255, 255, 1.0), border: Border.all(color: const Color(0x33A6A6A6)), borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: new Form( key: formKey, child: new Column( children: <Widget>[ new CustomTextField( inputBoxController: teFirstName, focusNod: _focusNodeFirstName, textSize: 12.0, textFont: "Nexa_Bold", ).textFieldWithOutPrefix( AppTranslations.of(context).text("key_first_name"), AppTranslations.of(context).text("key_enter_first_name")), new CustomTextField( inputBoxController: teLastName, focusNod: _focusNodeLastName, textSize: 12.0, margin: EdgeInsets.only(top: 20.0), textFont: "Nexa_Bold", ).textFieldWithOutPrefix( AppTranslations.of(context).text("key_last_name"), AppTranslations.of(context).text("key_enter_last_name")), new CustomTextField( inputBoxController: teEmail, focusNod: _focusNodeEmail, textSize: 12.0, margin: EdgeInsets.only(top: 20.0), textFont: "Nexa_Bold", ).textFieldWithOutPrefix( AppTranslations.of(context).text("key_email"), AppTranslations.of(context) .text("key_please_enter_valid_email")), new CustomTextField( inputBoxController: tePassword, focusNod: _focusNodePass, textSize: 12.0, isPassword: true, margin: EdgeInsets.only(top: 20.0, bottom: 20.0), textFont: "Nexa_Bold", ).textFieldWithOutPrefix( AppTranslations.of(context).text("key_password"), AppTranslations.of(context) .text("key_must_be_at_least_6_characters")), ], ), ), ), new Container( margin: EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0), child: buttonWithColorBg( AppTranslations.of(context).text("key_next"), EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0), const Color(0xFFFFD900), const Color(0xFF28324E)), ), ], ); return MaterialApp( debugShowCheckedModeBanner: false, theme: new ThemeData( primaryColor: const Color(0xFF02BB9F), primaryColorDark: const Color(0xFF167F67), accentColor: const Color(0xFF167F67), ), home: new Scaffold( backgroundColor: const Color(0xFFF1F1EF), appBar: new AppBar( title: new Text( label, style: new TextStyle(color: Colors.white), ), actions: <Widget>[ PopupMenuButton<String>( // overflow menu onSelected: _select, icon: new Icon(Icons.language, color: Colors.white), itemBuilder: (BuildContext context) { return languagesList .map<PopupMenuItem<String>>((String choice) { return PopupMenuItem<String>( value: choice, child: Text(choice), ); }).toList(); }, ), ], ), key: scaffoldKey, body: new Container( child: new SingleChildScrollView( child: new Center( child: signUpForm, ), ), ), ), ); } Widget buttonWithColorBg( String buttonLabel, EdgeInsets margin, Color bgColor, Color textColor) { var loginBtn = new Container( margin: margin, padding: EdgeInsets.all(15.0), alignment:, decoration: new BoxDecoration( color: bgColor, borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: Text( buttonLabel, style: new TextStyle( color: textColor, fontSize: 20.0, fontWeight: FontWeight.bold), ), ); return loginBtn; } }

8. In this example, we have a custom field class that we have created for example simplicity and reduce the number of the code line. You can use it as in other projects.

import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; class CustomTextField { final double outPrefixSvgWidth; final double outPrefixSvgHeight; final int outPrefixSvgColor; EdgeInsets margin; final TextEditingController inputBoxController; final bool isPassword; final FocusNode focusNod; final TextInputType keyBoardType; final TextAlign textAlign; final Widget prefix; final Widget suffix; final int textColor; final String textFont; final double textSize; final bool clickable; final int maxLength; CustomTextField( { this.outPrefixSvgWidth = 22.0, this.outPrefixSvgHeight = 22.0, this.outPrefixSvgColor, this.margin, this.inputBoxController, this.isPassword = false, this.focusNod, this.keyBoardType = TextInputType.text, this.prefix , this.suffix , this.textColor = 0xFF757575, this.textFont = "", this.textSize = 12.0, this.clickable = true, this.maxLength = 0, this.textAlign = TextAlign.left}); Widget textFieldWithOutPrefix(String hint, String errorMsg) { var loginBtn = new Container( margin: margin, child: new Row( crossAxisAlignment:, children: <Widget>[ textField(hint, errorMsg), ], ), ); return loginBtn; } Widget textField(String hint, String errorMsg) { FocusNode focusNode = focusNod != null ? focusNod : new FocusNode(); var list = maxLength == 0 ? null:[ LengthLimitingTextInputFormatter(maxLength), ]; var loginBtn = new EnsureVisibleWhenFocused( focusNode: focusNode, child: new Expanded( child: new TextFormField( obscureText: isPassword, controller: inputBoxController, focusNode: focusNode, keyboardType: keyBoardType, enabled: clickable, textAlign: textAlign, inputFormatters: list, decoration: InputDecoration( labelText: hint, hintText: hint, prefixIcon: prefix, suffixIcon: suffix, ), validator: (val) => val.isEmpty ? errorMsg : null, onSaved: (val) => val, ), flex: 6, )); return loginBtn; } } class EnsureVisibleWhenFocused extends StatefulWidget { const EnsureVisibleWhenFocused({ Key key, @required this.child, @required this.focusNode, this.curve: Curves.ease, this.duration: const Duration(milliseconds: 100), }) : super(key: key); /// The node we will monitor to determine if the child is focused final FocusNode focusNode; /// The child widget that we are wrapping final Widget child; /// The curve we will use to scroll ourselves into view. /// /// Defaults to Curves.ease. final Curve curve; /// The duration we will use to scroll ourselves into view /// /// Defaults to 100 milliseconds. final Duration duration; @override _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState(); } /// /// We implement the WidgetsBindingObserver to be notified of any change to the window metrics /// class _EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> with WidgetsBindingObserver { @override void initState() { super.initState(); widget.focusNode.addListener(_ensureVisible); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); widget.focusNode.removeListener(_ensureVisible); super.dispose(); } /// /// This routine is invoked when the window metrics have changed. /// This happens when the keyboard is open or dismissed, among others. /// It is the opportunity to check if the field has the focus /// and to ensure it is fully visible in the viewport when /// the keyboard is displayed /// @override void didChangeMetrics() { if (widget.focusNode.hasFocus) { _ensureVisible(); } } /// /// This routine waits for the keyboard to come into view. /// In order to prevent some issues if the Widget is dismissed in the /// middle of the loop, we need to check the "mounted" property /// /// This method was suggested by Peter Yuen (see discussion). /// Future<Null> _keyboardToggled() async { if (mounted) { EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets; while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) { await new Future.delayed(const Duration(milliseconds: 10)); } } return; } Future<Null> _ensureVisible() async { // Wait for the keyboard to come into view await Future.any([ new Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled() ]); // No need to go any further if the node has not the focus if (!widget.focusNode.hasFocus) { return; } // Find the object which has the focus final RenderObject object = context.findRenderObject(); final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); assert(viewport != null); // Get the Scrollable state (in order to retrieve its offset) ScrollableState scrollableState = Scrollable.of(context); assert(scrollableState != null); // Get its offset ScrollPosition position = scrollableState.position; double alignment; if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) { // Move down to the top of the viewport alignment = 0.0; } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset) { // Move up to the bottom of the viewport alignment = 1.0; } else { // No scrolling is necessary to reveal the child return; } position.ensureVisible( object, alignment: alignment, duration: widget.duration, curve: widget.curve, ); } @override Widget build(BuildContext context) { return widget.child; } }

Now, merge all files and run the example. You can see the app running very smoothly as shown above. 

Flutter mutli language               Flutter localization apk

But if you are facing any problem or you have any quires, please feel free to ask it from below comment section.


We're Social