Add multiple languages to your Flutter app

Mukul AryalMukul Aryal
13 min read

Whenever you build a mobile application, you may not have to care about adding multiple languages into your app. But, if you are creating an application that needs to focus on a specific locality or country, it is pivotal to make your application available in multiple languages so that you don’t miss out on potential users avoiding the app due to language barriers. In this blog post, I will guide you through the process of adding multiple language support to your Flutter application.

What we will build

This article will guide you step by step through the process of creating a complete application with the following features:

  • Multiple languages support

  • A settings page that has an option to change the language

  • Reflect the changes made in the Settings page throughout the app

  • Persist the users’ desired languages in future sessions

    GIF image of the final app

You will notice the application also has a toggle to select the theme. Theme selection will not be covered in this article but it also follows most of the processes described in the article. The full code for the final application can be found on GitHub in the flutter-base-app GitHub repository.

Disclaimer
Before you proceed, the blog post assumes that you have a basic understanding of Flutter and its workings. If you are unfamiliar with basic Flutter concepts, I recommend you go through the official Learn Flutter part of the Flutter documentation. The code snippets included in the blog post have been tested on Flutter 3.29 along with Dart version 3.7 on an Android device running Android 15. Some code snippets may need some tweaking depending on your development setup. Any concepts related to localization in the article will be clearly explained in a beginner-level language. We will skim through other topics like global state management and preference persistence.

Introduction

In the early days of mobile application development, you could assume that everyone who is tech literate also has at least a basic understanding of the English language. But in today’s growing digital landscape, more and more people have access to technology even if they only speak one language.

So, it is extremely important to design your software to be adaptable to various languages. This process of creating a “language modular“ software product is known as internationalization. Whereas, adding a specific language to your pre-existing software product in called localization. Internationalization and localization are often abbreviated as i18n and l10n respectively for convenience. These abbreviations may seem weird at first, but seem perfectly normal when you realize there are 18 characters between i and n in internationalization and there are 10 characters between l and n in localization.

Installing packages

To build the full application with settings page, you will need to install three packages.

  • flutter_localizations and intl for all the l10n code.

  • provider to handle the global state management.

  • shared_preferences to persist the users’ language choice.

You can install all the packages through the command line easily, but for the localization packages, please use the following command:

$ flutter pub add flutter_localizations --sdk=flutter
$ flutter pub add intl:any

After installing all required packages, manually enable the generate flag in the flutter section of the pubspec.yaml file. This will enable our code to generate usable localizations. Finally, your pubspec.yaml file should look like this:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  cupertino_icons: ^1.0.8
  intl: any
  provider: ^6.1.2
  shared_preferences: ^2.3.2

# Other options........
flutter:
    generate: true

Localization

Basic Setup

After all packages have been installed, open your main.dart file and import the flutter_localizations package.

import 'package:flutter_localizations/flutter_localizations.dart';

Then, in your MaterialApp or CupertinoApp object, specify the supportedLocales and localizationsDelegates.

return MaterialApp(
      title: 'Flutter Base App',
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [Locale('en'), Locale('ne')],
      locale: const Locale('en'),
      // Other parameters
)

Note: You may want to comment out the AppLocalizations.delegate item in the list. This will throw an error until we have created the specific translated strings (which we will do in a while).

What is happening here?

Localizing an application is not as simple as basic string replacement. How a language is written and presented depends highly on the specific language. For example, Arabic is written right-to-left while majority of other languages are written left-to-right.

The localizationDelegates parameter specifies who is responsible for providing the various translations and behaviours.

  • AppLocalizations is responsible for providing the specific string translation (which we will generate and import in a moment).

  • Global[X]Localizations are responsible for handling widget-specific behaviors based on the language.

The supportedLocales parameter is very straight forward, it is a list of languages your application supports. To add any new language, add the language code within the Locale object in the list. The language codes used follows the two letter ISO 639-1 language codes standard. As you can see, my current app supports two languages; English and Nepali.

For now, we are currently setting the default language as English with the locale: Locale('en') parameter. You may change it if you want

Creating necessary files and folders

In the root directory of your project (on the same level as pubspec.yaml), create a new file l10.yaml. Also, create a new folder named l10n inside the lib directory. Create a new file app_en.arb inside the newly created l10n folder.

The l10.yaml file

The l10.yaml file is basically a config file that tells flutter where to look for the string translations and where to store the generated usable strings. Add the following lines to the l10.yaml file:

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
  • arb-dir specifies where to find the arb files. We will expand on what arb is in a little bit.

  • tempale-arb-file is used to locate the template file for the translations. Again, we will expand on this in the next section.

  • output-localiztion-file specifies the name of the module we can import to finally use the localizations.

What is app_en.arb?

First of all, arb is an abbreviation for App Resource Bundle. It simply a JSON-like file that will store the localization strings. We have specified app_en.arb as the special “template file“ meaning that this file will contain localization strings as well as define the “keys“ that all other arb files will use. Simply put, you can only use a specific key in the Nepali arb if the key is defined in the English arb.

Note: The naming convention for the arb files is app_languageCode.arb.

Since, app_en.arb is the template file, add the following sample key value pairs in the file:

{
    "@@locale": "en",
    "mainScreenTitle": "This is the main screen",
    "@mainScreenTitle": {
        "description": "The title of the main screen"
    },
    "settingsButtonText": "Settings",
    "@settingsButtonText": {
        "description": "The text of the settings button"
    },
    "settingsScreenTitle": "This is the Settings screen",
    "@settingsScreenTitle": {
        "description": "The title of the settings screen"
    },
    "settingsLanguageLabel": "Language",
    "@settingsLanguageLabel": {
        "description": "The label of the language setting"
    }
}

In this file, we have specified many normal looking “key“: “value“ pairs but you may also notice some weird looking “@key“: ”value” pairs. These special keys define the specific keys all other arbs will have to use. Also, other “@@key“: “value“ pairs are used to specify metadata.

Now, since we have the template arb file, we can proceed to creating arb files for our Nepali localization strings. For that, we will create a new file app_ne.arb in the same directory as app_en.arb with the following content:

{
    "@@locale": "ne",
    "mainScreenTitle": "यो मुख्य पृष्ठ हो",
    "settingsButtonText": "सेटिङ्स",
    "settingsScreenTitle": "यो सेटिङ्स पृष्ठ हो",
    "settingsLanguageLabel": "भाषा"
}

Notice how this file is much simpler than the template file and contains only those keys we specified with @key in the template arb file. This example shows a very simple usage of the localization strings, if you want to explore other complex things like placeholders, refer to the official i18n Flutter docs.

Using the localization strings

Make sure to hot restart your applications which will create the usable localization files. Firstly, in the main.dart file, import the generated files.

import 'package:flutter_gen/gen_l10n/app_localizations.dart';

After this, you may un-comment the AppLocalizations.delegate which we made sure to comment out earlier.

Now, import the app_localizations.dart file where ever you want to use localization strings, and place them where you would normally use constant strings.

For example, in my main screen instead of using

// Other code
child: Text("This is the main screen")

I would use

// Other code
child: Text(AppLocalizations.of(context)!.mainScreenTitle))

The attribute of the AppLocalizations you can use depends on the keys you defined in the arb files.

To check if everything is working, you can manually update the locale paramater in your MaterialApp or CupertinoApp.

Settings Page

Now that we have added multiple languages to our Flutter app, we want a way for the user to choose what language they want to view the language in. This can be presented as an option to the user during the first start of the application as well. In this article, we will create a simple settings page with one toggle to change the language.

Firstly, we will create a simple settings page with a drop down menu with the choice to select the language. If you do not know how to create a drop down menu, the DropdownMenu (Widget of the Week) video on the official Flutter YouTube channel is a great resource.

Screenshot of the settings page

Make sure you are using the localization strings here as well instead of using constant strings. I have taken the liberty to add a label and icon alongside the drop down menu to make things visually appealing.

Each DropdownMenuItem within the DropdoownButton has a value property. The value of each button is passed to the onChanged function. So the value for each corresponding item is the language code for the language it represents. Write a placeholder function for now.

Currently, the app does nothing whenever we select a new language. This seems to be a challenging task as we need to change the value of the locale parameter on our MaterialApp or CupertinoApp from within the settings page. For this, we need to take the help of global state management.

Global State Management

There are many packages you can use for global state management. For the purposes of this tutorial, we are using the very beginner friendly provider package. If you are not familiar with this package, I highly recommend reading Simple app state management from the official Flutter docs.

In the lib folder, create a new folder named providers. This folder will contain all the provider classes we will use for global state management. Create a file language_provider.dart within this new folder.

Add the following code to the new file:

import 'package:provider/provider.dart';

class LanguageProvider extends ChangeNotifier {
  String languageCode;

  LanguageProvider({required this.languageCode});

  void setLanguage({required String newLanguageCode}) async {
    languageCode = newLanguageCode;
    notifyListeners();
  }
}

What is happening here?

The code here is very simple to understand. Here is a line by line breakdown of the code:

  • First of all, we have imported the provider package.

  • We have created a new class LanguageProvider which inherits from the ChangeNotifier class.

  • The class only has a single attribute; a string named languageCode.

  • The constructor has a compulsory requirement of a languageCode.

  • The setLanguage function is an asynchronous function that takes in a newLanguageCode parameter and updates the current attribute with the new language code. Finally, it notifies all widgets that are “listening“ to this provider and forces them to update their states.

Updating the settings page

In the settings page, import the newly created provider along with the base provider package.

import 'package:flutter_base_app/providers/language_provider.dart';
import 'package:provider/provider.dart';

In the onChanged parameter which we left empty earlier, add the following code:

void __languageHandler(String? value) async {
    if (value != null) {
        context
            .read<LanguageProvider>()
            .setLanguage(newLanguageCode: _language);
    }
  }
// Other code....
onChanged: __languageHandler;

What is happening here?

In the function, the following this are happening:

  • We are taking the new value provided by the change in the drop down menu.

  • If the value is not null, we are tapping into our LanguageProvider provider and updating the current language code.

Updating the locale

Currently, we are successfully broadcasting the change in language to all listeners. We now need a listener that can react to these changes. The appropriate place for these changes to happen is in the locale parameter of our MaterialApp or CupertinoApp. In the main.dart file, make the following changes:

// Import the appropriate provider and base provider class
import 'package:flutter_base_app/providers/language_provider.dart';
import 'package:provider/provider.dart';
// Other Code....

void main(){
// Other Code....
runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => LanguageProvider(languageCode: 'en'))
      ],
      child: const MainApp(),
    ),
  );
}

// Other Code...

return MaterialApp(
    // Other Parameters....
    locale: Locale(context.watch<LanguageProvider>().languageCode),
    // Other Parameters.....
)

What is happening here?

The following changes have been made to main.dart:

  • The appropriate provider and base provider package have been imported.

  • In the runApp function, rather than supplying the main app directly, it has first been wrapped in a MultiProvider object with out new LanguageProvider. The default language when the app runs first is set to English as a language code is required by the class. The MultiProvider object helps to define the scope till where our providers can broadcast their messages.

  • Finally, rather than supply a default language code string in the Locale object, we tap into the languageCode attribute of the LanguageProvider.

Now, you can go to the settings page to choose a different language and you will notice that the language of the application changes as you please!

But, once you restart the app, the changes you made are lost. For this, we will have to save the user preferences somewhere.

Persisting the changes

We have already installed the shared_preferences package. This package allows us to store key value pairs on disk. It is very similar to the window.localStorage function you would find on websites. If you want to learn more about this package, I would recommend you read the Store key-value data on disk section of the official Flutter docs.

Updating the settings page

Make the following changes to your settings page:

import 'package:shared_preferences/shared_preferences.dart';

void __languageHandler(String? value) async {
    final prefs = await SharedPreferences.getInstance();
    if (value != null) {
      setState(() {
        _language = value;
        context
            .read<LanguageProvider>()
            .setLanguage(newLanguageCode: _language);
      });
    }
    await prefs.setString('language', _language);
  }

What is happening here?

We have made the following changes to the settings page:

  • We have imported the shared_preferences package.

  • The __languageHandler function has been made asynchronous.

  • Inside the function, we have:

    • Created a new instance of the SharedPreferences key value store.

    • After the state of the language has been updated, we have persisted the new value of the language in the language key of the key value store.

Updating main.dart

Finally, in main.dart we have to make some very small changes.

import 'package:shared_preferences/shared_preferences.dart';

void main() async {

  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  // Get language config from shared preferences if available
  final String? languageCode = prefs.getString('language');
  runApp(
    MultiProvider(
      providers: [
        // if shared preferences is empty, default value (english lang) will be used
        ChangeNotifierProvider(create: (context) => LanguageProvider(languageCode: languageCode ?? 'en'))
      ],
      child: const MainApp(),
    ),
  );
}

What is happening here?

The following changes have been made:

  • We have imported the shared_preferences package.

  • The main function has been made asynchronous.

  • Inside the function, we have:

    • Created a new instance of the SharedPreferences key value store.

    • Fetched the value of the language key from the key value store in the languageCode variable.

    • If languageCode is null, we will use the default language of English, otherwise we will choose whatever language the user last chose in the settings page.

And voila! Everything we set out to do works!

Conclusion

Congratulations! You have reached the end of this blog post. I hope that this post was helpful to you. If you hope to read more posts like this, consider following me on Hashnode, and joining the newsletter.

If you have any queries or suggestions, please leave a comment or reach out to me directly by sending an email to mukul.development@gmail.com.

0
Subscribe to my newsletter

Read articles from Mukul Aryal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mukul Aryal
Mukul Aryal