Flutter localization and internationalization i18n with examples

Flutter localization and internationalization (i18n) with examples

If you are a mobile developer, you may have already got the hang of Flutter. It is Google’s open-source UI software development toolkit for building beautiful applications, that are natively compiled for mobile as well as for web and desktop, from a single codebase. The first version of Flutter, which was known as “Sky”, only ran on Android OS, but now you can develop applications for iOS, Linux, Mac, Windows, and Google Fuchsia, too. All in all, Flutter internationalization (or i18n) is an important topic for any Flutter developer.

You might be also interested in checking out our Flutter SDK, which provides over-the-air support for your apps.

    Flutter internationalization and localization

    Prerequisites

    In this article, we are going to see how to introduce Flutter i18n into an application. Before diving into the internationalization part, however, you will need to set up your working environment by following the official guide at docs.flutter.dev.

    This tutorial also assumes that you have basic knowledge of Flutter.

    Once you have installed everything, just create a new Flutter application by running:

    flutter create i18n_demo

    We also need content to work with, so let’s do some more preparations.

    Source code can be found on GitHub.

    Preparing the Flutter app

    We are going to create most of the files inside the lib folder so unless stated otherwise you should work within that folder.

    So, let’s open lib and tweak the main.dart file in the following way:

    import 'package:flutter/material.dart';
    import 'package:i18n_demo/app/my_app.dart';
    
    void main() {
      runApp(const MyApp());
    }

    Next, inside, create an app folder with a my_app.dart file:

    import 'package:flutter/material.dart';
    import 'package:i18n_demo/app/pages/my_home_page.dart';
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter i18n',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(),
        );
      }
    }

    Finally, in the app folder, create a new pages directory with a my_home_page.dart file:

    import 'package:flutter/material.dart';
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key});
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text("Welcome!"),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text("Press the button below"),
                Text(
                  "Times pressed: $_counter",
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    

    Here we are displaying a button that can be pressed and the counter will increment accordingly.

    Now we are ready to proceed to the main part!

    Internationalization libraries

    So, the first step is adding two dependencies to our app — namely, the flutter_localizations and the intl libraries — that will do some heavy lifting for us.

    Therefore, run these two commands:

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

    After everything is installed, open the pubspec.yaml file in the project root, find the flutter section, and tweak it by adding the generate setting:

    # The following section is specific to Flutter packages.
    flutter:
      generate: true

    Finally, in the project root, create a new file called l10n.yaml with the following contents:

    arb-dir: lib/l10n
    template-arb-file: intl_en.arb
    output-localization-file: app_localizations.dart

    Here we are saying where our translation files will be located, what the template (main) file is, and where the compiled translations should be placed. There are many other options that you can tweak — learn about them in the official docs.

    That’s it! Now let’s take care of the translation files.

    Creating Flutter translation files

    ARB files

    Translations for Flutter apps are usually stored within ARB files. These are relatively simple key-value files that will then be compiled into special Dart files consumed by the app.

    We’ll start by creating an l10n directory within the lib folder. Inside, I’m going to create two files: intl_en.arb and intl_ru.arb to store English and Russian translations. Of course, you can pick almost any other locale as currently Flutter supports more than a hundred languages.

    Let’s provide the English text first:

    {
      "@@locale": "en",
      "appTitle": "Flutter i18n",
      "@appTitle": {
        "description": "Main application title"
      },
      "welcome": "Welcome!"
    }

    A few things to note here:

    • @@locale specifies the language within this file
    • appTitle and welcome are the translation keys that we are going to use within our source code. Their corresponding values are the actual translations.
    • @appTitle is the metadata for the appTitle key. In the example above, it contains the key description. While it’s not mandatory to provide metadata, you’ll usually want to do it to explain the purpose of this key and provide additional context for your translators.

    Now let’s tweak the intl_ru.yaml file:

    {
      "@@locale": "ru",
      "appTitle": "Flutter i18n",
      "welcome": "Добро пожаловать!"
    }

    In this file, we don’t normally provide any metadata.

    Compiling Dart files from ARB

    While you or your translators will work with the ARB files, Flutter requires translations to be provided in a different format. To be precise, we should convert the ARBs into regular Dart files, so let’s run the following command:

    flutter gen-l10n

    Make sure to rerun this command every time you update your translations.

    You’ll notice that a new .dart_tool\flutter_gen\gen_l10n folder will be created in the project root. It contains three files: app_localizations.dart, app_localizations_en.dart, and app_localizations_ru.dart. The first file stores information about all the supported locales, as well as exposing some useful methods. The two other files simply store your translations.

    Please note that you are not expected to modify these files manually as they will be recompiled every time the flutter gen-l10n command is executed.

    Performing simple translations

    All right, so now that we have two translation keys it’s time to use them in the source code.

    Translating the app title

    First, I’d like to display the app title. Open the app/my_app.dart file and import the app_localizations.dart:

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

    Next, we need to provide information about the supported locales and configure delegates, so make the following changes within the same file:

    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter i18n',
          localizationsDelegates: AppLocalizations.localizationsDelegates, // <--- add this
          supportedLocales: AppLocalizations.supportedLocales, // <--- add this
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(),
        );
      }
    }

    Finally, replace the title with the onGenerateTitle:

    class MyApp extends StatelessWidget {
      // ...
    
      Widget build(BuildContext context) {
        return MaterialApp(
          onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, // <---
          // ...
        );
      }
    }

    In this code sample, you can see how the translation keys are utilized in the code. Specifically, you reference the AppLocalizations and then call the property named after your translation key (appTitle in our case).

    Displaying the welcoming message

    Now let’s see another example. Open the app/pages/my_home_page.dart file and add the following import:

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

    Then, simply tweak your widget by adjusting its title:

    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        // ...
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(AppLocalizations.of(context)!.welcome), // <--- add this
          ),
          body: Center(
            // ...
          ),
          // ...
        );
      }
    }

    Note that in this case we don’t need to use the onGenerateTitle (in fact, it’s only needed for the global title).

    That’s it! You can now run your Flutter app on a mobile phone or emulator to make sure that everything is running smoothly. If you change the system locale in the phone settings, the app will be translated accordingly.

    Interpolation and placeholders in Flutter translations

    Of course, that’s not it. At this point, let’s see how to interpolate custom values within your translations.

    Inserting custom text

    Let’s suppose that we would like to display copyright information and embed the company name in it. Tweak the intl_en.arb file within the lib/l10n directory:

    {
      "@@locale": "en",
    
      // ...
    
      "createdBy": "Tutorial by {company}",
      "@createdBy": {
        "description": "Copyright message",
        "placeholders": {
          "company": {
            "type": "String",
            "example": "Demo"
          }
        }
      }
    }

    {company} is a placeholder for which we can provide the value within the source code. Inside the metadata, we can also explain what the expected type of this value is and show an example.

    Let’s adjust the intl_ru.arb file:

    {
      "@@locale": "ru",
      
      // ...
    
      "createdBy": "Руководство от {company}"
    }

    Please note that the placeholder’s name must stay the same, whereas the translation text can be modified as needed.

    Now, remember to run the flutter gen-l10n command.

    Next, open the app/pages/my_home_page.dart file and provide a new text widget:

    class _MyHomePageState extends State<MyHomePage> {
      // ...
    
      Widget build(BuildContext context) {
        return Scaffold(
          // ...
          body: Center(
            child: Column(
              // ...
              children: <Widget>[
                Text(AppLocalizations.of(context)!.createdBy('Lokalise')), // <--- add this
                const Text("Press the button below"),
                // ...
              ],
            ),
          ),
          // ...
        );
      }
    }

    As you can see, the approach is similar but in this case we have to provide the company name as an argument.

    In fact, the createdBy method looks like this:

      String createdBy(String company) {
        return 'Tutorial by $company';
      }

    So, it’s nothing too complex, really.

    Displaying the currently set locale

    We can use interpolation to display the currently set locale as well.

    First, let’s create a new current_locale_widget.dart in the app/widgets folder:

    import 'package:flutter/material.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    
    class CurrentLocaleWidget extends StatelessWidget {
      const CurrentLocaleWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        final locale = Localizations.localeOf(context);
    
        return Center(
            child: Text(
          AppLocalizations.of(context)!.currentLocale(locale.toString()),
          style: Theme.of(context).textTheme.headlineMedium,
        ));
      }
    }
    

    The currently set locale can be easily fetched by running the localeOf method. Then, we simply display it using the currentLocale method, which will be created in a moment.

    Let’s import and display this widget inside the my_home_page.dart:

    // ...
    import 'package:i18n_demo/app/widgets/current_locale_widget.dart'; // <--- add this
    
    // ...
    
    class _MyHomePageState extends State<MyHomePage> {
      // ...
    
      Widget build(BuildContext context) {
        return Scaffold(
          // ...
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const CurrentLocaleWidget(), // <--- add this
                Text(AppLocalizations.of(context)!.createdBy('Lokalise')),
                // ...
              ],
            ),
          ),
          // ...
        );
      }
    }
    

    Finally, add English translation in the intl_en.arb file:

    {
      // ...
    
      "currentLocale": "Current locale is {locale}"
    }

    And the Russian one in the intl_ru.arb file:

    {
      // ...
    
      "currentLocale": "Текущая локаль: {locale}"
    }

    Great job!

    More Flutter i18n features

    Date and time localization

    Let’s move on to localizing date and time in Flutter.

    First, add a new Text widget right before or after the const CurrentLocaleWidget() in the my_home_page.dart file:

    // ...
    
    children: <Widget>[
      const LanguageWidget(),
      Text(AppLocalizations.of(context)!.currentDate(DateTime.now())), // <--- add this
      Text(AppLocalizations.of(context)!.pressButton),
      // ... other text widgets here...
    ],
    
    // ...

    So, we are simply displaying the current date and time. However, as you know, there can be many different formats to represent this information. Luckily, the DateFormat class provided by the intl package has our back, and we simply need to choose the format that works for us.

    For instance, let’s display the current year, month, weekday, and day. Tweak the intl_en.arb in the following way:

    {
      // ...
    
      "currentDate": "Today is {date}",
      "@currentDate": {
        "description": "Localized current date",
        "placeholders": {
          "date": {
            "type": "DateTime",
            "format": "yMMMMEEEEd"
          }
        }
      }
    }

    yMMMMEEEEd is the predefined format available in the DateFormat class. Note that it must be provided in the metadata.

    Inside the intl_ru.arb file, we can omit the metadata to use the same format:

    {
      // ...
    
      "currentDate": "Сегодня {date}"
    }

    That’s it!

    Localizing numbers and currencies

    The next step is localizing number and currency information.

    Let’s add yet another Text widget inside the my_home_page.dart file:

    // ...
    
    children: <Widget>[
      const LanguageWidget(),
      Text(AppLocalizations.of(context)!.currentDate(DateTime.now())), 
      Text(AppLocalizations.of(context)!.currencyDemo(1234567)), // <--- add this
      // ... other text widgets here...
    ],
    
    // ...

    Flutter has a few predefined number formats for us, so you can pick any that suit you or find more information in the NumberFormat class docs.

    Tweak the intl_en.arb file, like so:

    {
      // ...
    
      "currencyDemo": "Here's a demo price: {value}",
      "@currencyDemo": {
        "description": "A demo showing how to localize currency",
        "placeholders": {
          "value": {
            "type": "int",
            "format": "currency",
            "optionalParameters": {
              "decimalDigits": 2
            }
          }
        }
      }
    }

    In this example we are employing the currency format, which uses the following pattern: <CURRENCY_NAME><FORMATTED_NUMBER>. For the English locale the default currency is USD, whereas for Russian it’s RUB. We also ask to add two decimal digits.

    Now, provide the translation in the intl_ru.arb file:

    {
      // ...
    
      "currencyDemo": "Демонстрационная цена: {value}"
    }

    Great — our currency information is properly localized!

    Pluralization

    Another important area is pluralization, which means adjusting the text based on a count. As you likely remember, we have a button that can be pressed to increment the counter. Let’s try to adjust the displayed text according to the counter’s value.

    First, tweak the Text widgets in the my_home_page.dart file once again:

    children: <Widget>[
      const CurrentLocaleWidget(),
    
      // ...
    
      Text(AppLocalizations.of(context)!.pressButton), // <-- change this
    
      Text(
       AppLocalizations.of(context)!.buttonPressed(_counter), // <-- change this
       style: Theme.of(context).textTheme.headlineMedium,
      ),
    ],

    Now open the intl_en.arb file and add the following:

    {
      "pressButton": "Press the button below",
      "buttonPressed": "{count, plural, =0{Has not pressed yet} =1{Pressed 1 time} other{Pressed {count} times}}",
      "@buttonPressed": {
        "description": "Shows how many times the button has been pressed (pluralized)",
        "placeholders": {
          "count": {
            "type": "num",
            "format": "compact"
          }
        }
      }
    }

    As you can see, for the buttonPressed key we are using a special plural expression that will display one of the texts based on the value of count. Specifically, there are three possible cases: when the count equals zero, when it equals one, and when it has some other value.

    By the way, plural is not the only expression supported by Flutter. For example, there’s a select expression that can come in really handy when working with gender information, as shown here:

    "pronoun": "{gender, select, male{he} female{she} other{they}}",
    "@pronoun": {
      "description": "A gendered message",
      "placeholders": {
        "gender": {
          "type": "String"
        }
      }
    }

    Finally, let’s add the translations within the intl_ru.arb file:

    {
      // ...
    
      "pressButton": "Нажмите кнопку ниже",
      "buttonPressed": "{count, plural, =0{Не было нажатий} =1{Нажата 1 раз} few{Нажата {count} раза} other{Нажата {count} раз}}"
    }

    Different languages have different pluralization rules, so in this case we have to provide not three but four options (these are called plural forms).

    And that’s it! We have translated our app and now you can rerun it to make sure that everything works.

    Switching the locale programmatically

    While the app language can be changed by switching the system locale, this is not always convenient. Therefore, let’s see a simple way to add a locale switcher to the app.

    Adding a locale provider

    First, we’ll need to add a new dependency, called provider, that will greatly help us. Run the following command:

    flutter pub add provider

    Next, let’s create a locale_provider.dart file inside the app/providers directory:

    import 'package:flutter/material.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    
    class LocaleProvider extends ChangeNotifier {
      Locale _locale = const Locale("en");
    
      Locale get locale => _locale;
    
      void setLocale(Locale locale) {
        if (!AppLocalizations.supportedLocales.contains(locale)) return;
    
        _locale = locale;
        notifyListeners();
      }
    }
    

    There are two main things to note here:

    • Our default locale is set to en.
    • Inside the setLocale method, we check that the requested locale is actually supported, then switch it and notify the listeners of this.

    The final thing to do is use this provider in the my_app.dart file. Add these imports:

    import 'package:i18n_demo/app/providers/locale_provider.dart';
    import 'package:provider/provider.dart';

    We then need to wrap the main part of the code in the ChangeNotifierProvider, so your file will now have the following content:

    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) => ChangeNotifierProvider( // <--- add this
          create: (context) => LocaleProvider(), // <--- add this
          builder: (context, child) { // <--- add this
            final provider = Provider.of<LocaleProvider>(context); // <--- add this
    
            return MaterialApp(
              localizationsDelegates: AppLocalizations.localizationsDelegates,
              supportedLocales: AppLocalizations.supportedLocales,
              locale: provider.locale, // <--- add this
              onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
              theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
                useMaterial3: true,
              ),
              home: const MyHomePage(),
            );
          });
    }

    Also note that we are providing locale here, and read the currently set locale from the provider.

    Creating a locale switcher

    So, our provider is now complete; therefore, create a locale_switcher_widget.dart file in the app/widgets folder. Add the following imports inside it:

    import 'package:flutter/material.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    import 'package:i18n_demo/app/providers/locale_provider.dart';
    import 'package:provider/provider.dart';

    Then code the widget itself:

    class LocaleSwitcherWidget extends StatelessWidget {
      const LocaleSwitcherWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        final provider = Provider.of<LocaleProvider>(context);
        final locale = provider.locale;
    
        return DropdownButtonHideUnderline(
          child: DropdownButton(
            value: locale,
            icon: Container(width: 12),
            items: AppLocalizations.supportedLocales.map(
              (nextLocale) {
                return DropdownMenuItem(
                  value: nextLocale,
                  onTap: () {
                    final provider =
                        Provider.of<LocaleProvider>(context, listen: false);
    
                    provider.setLocale(nextLocale);
                  },
                  child: Center(
                    child: Text(nextLocale.toString()),
                  ),
                );
              },
            ).toList(),
            onChanged: (_) {},
          ),
        );
      }
    }

    What we are doing here is reading the currently set locale and then drawing a dropdown. Inside, we are displaying all the supported locales and adding an onTap event that calls the setLocale() method in our provider. Basically, that’s it.

    Of course, we also have to display the widget itself, so import it in the my_home_page.dart file:

    import 'package:i18n_demo/app/widgets/locale_switcher_widget.dart';

    Then simply add actions to the appBar:

    class _MyHomePageState extends State<MyHomePage> {
      // ...
    
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            // ...
            actions: const [ // <--- add this
              LocaleSwitcherWidget(),
              SizedBox(width: 12),
            ],
          ),
          body: Center(
            // ...
          ),
          // ...
        );
      }
    }

    Awesome! Now reload the app and try to switch locale using the dropdown in the top menu.

    Use Lokalise AI to translate into multiple locales

    As you can see, managing translation files can be quite tedious, especially for larger applications. Moreover, you might need to add support for languages that you can’t really speak. In this case, you’ll need either to hire a professional or use AI. Meet Lokalise, the translation management system that takes care of all your Flutter internationalization needs and provides full support for ARB files. With features like:

    • Easy integration with various other services (GitHub, Figma, Asana, and many others)
    • Collaborative translations
    • Translation quality assurance tools
    • Easy management of your translations through a central dashboard
    • Plus, loads of others

    Lokalise will make your life a whole lot easier by letting you expand your Flutter app to all the locales you ever plan to reach.

    Uploading ARB translation files

    To get started, sign up for a free trial (no credit card information required). In your projects dashboard click the New project button:

    In the dialog, choose Software localization:

    Next, give your project any name, choose English (or any other language that is the default one for your app) as the Base language, and choose one or more Target languages (the ones to translate into):

    Let’s suppose, for instance, that I would now like to translate into French in addition to Russian.

    Then hit Proceed.

    On the next screen, click Upload files:

    Choose the ARB files from your project (don’t upload Dart files!). In fact, you can just upload the intl_en.arb, unless you want to adjust the Russian translations as well.

    If you are using pluralization in your ARB files, be sure to check the Detect ICU plurals option (because the plural is actually an ICU expression):

    The main part of the screen will look like this:

    What I also recommend doing is adding a special %LANG_ISO% placeholder in the filename (if it has not been added for you already). Click on the filename and provide the following:

    Adding this placeholder is beneficial because it will be replaced with the actual locale code once the file is exported back to the project (otherwise, all the translation files will have identical names).

    Press Import files once you are ready.

    Now, click Editor in the top menu and observe your translations:

    You might notice that the buttonPressed key does not contain the zero plural form. Why is that? Well, because it’s actually not a standard form for English. By default, this language only has two forms: one and other. If you need additional forms, click on the languages dropdown in the top menu, find the desired locale, and choose Settings:

    Then, simply enable the Custom plural forms switch and adjust the forms as needed:

    Next, simply re-upload the translation file while enabling the Detect ICU plurals and Replace modified values options.

    Of course, you can edit these texts manually, or you can take advantage of Lokalise AI to perform translations for you.

    Using Lokalise AI

    So, let’s employ Lokalise AI to translate into French. Choose all your translation keys by checking the box in the top left corner:

    Select Create a task from the dialog and hit Proceed:

    Pick the Automatic translation task type, give it a name, and optionally provide additional instructions for the AI:

    Click Next.

    Choose the Source language (English in my case) and one or more Target languages to translate into:

    Press Create task and wait a little while the AI does the job for you. You’ll get a notification email once everything is done.

    Now return to the editor and observe the result:

    Note by the way that the AI has even properly detected the French plural forms and provided correct translations for both one and other. Still, we recommend double-checking all the translations generated by AI because they are not always 100% ideal.

    Once everything is ready, you can download your translations back to the Flutter project.

    Downloading translation files

    Click Download in the top menu and choose Flutter (.arb) from the File format dropdown:

    Be sure not to pick Flutter SDK because this feature relates to OTA, which I’ll briefly explain soon.

    Now pick one or more languages to include in the download bundle. Since I have only amended the French translations, I’ll choose one language:

    Then click Build and download:

    You will get an archive containing your translation files. Extract it into the l10n folder of the project and don’t forget to run the flutter gen-l10n command again.

    Now run the app and make sure that it has support for three languages. How cool is that?

    Introducing the over-the-air flow

    Actually, Lokalise can do much more than that. Specifically, we offer a very cool feature that is called over the air (OTA). In a nutshell, it enables you to deliver updated translations to mobile app users without the need to release a new version of the app itself. Sound like magic? Well, perhaps, but you can easily set it up in your Flutter project.

    If you are interested in this feature, check out this tutorial that I’ve prepared for you. It covers all the necessary details that will help you take full advantage of OTA.

    Conclusion

    So, in this article we have learned how to translate Flutter apps into multiple languages, how to perform pluralization and localization, and how to programmatically set the locale. On top of that, we have utilized AI to translate our app into a new language. Great job!

    You may also be interested in reading our article on Android localization.

    Thank you for staying with me today and happy coding!

    Talk to one of our localization specialists

    Book a call with one of our localization specialists and get a tailored consultation that can guide you on your localization path.

    Get a demo

    Related posts

    Learn something new every two weeks

    Get the latest in localization delivered straight to your inbox.

    Related articles
    Localization made easy. Why wait?