A Guide to Flutter Localization

We untangle Flutter localization and internationalization so you can get back to the fun of Flutter app dev.

Flutter has come a long way since we last published How to Internationalize a Flutter App Using intl and intl_translation in late 2018. Since then, the cross-platform framework has taken the mobile app development world by storm. With 150,000 apps and half a million developers in its portfolio, it seems that Flutter—and its delightful development experience—have become a go-to for many developing iOS and Android apps (not to mention web apps). And with the recent Flutter 2 release, the promise of one codebase that works on multiple platforms goes even further: at the time of writing, Flutter 2 supports desktop (Linux, macOS, and Windows) at a near-stable level.

A bit of an unsung hero, the Flutter internationalization (i18n) team has made strides to streamline the first-party localization (l10n) experience over the last couple of years. When we wrote our first Flutter i18n guide, there was an annoying amount of boilerplate, and constant running of CLI tools to set up and add localization to a Flutter app. Well, no more! Automatic code generation has done away with most of these headaches.

In this article, we’ll show you how to use Flutter’s native localization package to localize your mobile apps for Android and iOS. We won’t cover web or desktop here, although it seems they should work in largely the same way.

📖 Go deeper » Flutter’s native localization package is is built on the first-party Dart intl package.

🗒 Note » If you want us to write about Flutter Web and/or Desktop i18n, let us know in the comments below.

Demo App

To keep things grounded and fun, we’ll build a small demo app and localize it: Heroes of Computer Science presents a selection of notable figures in the relatively short history of computing.

In English; soon in other languages

Versions Used

We’re using the following language, framework, and package versions in this article:

  • Dart 2.12.2
  • Flutter 2.0.3
  • flutter_localizations (version seems tied to Flutter) — provides localizations to common widgets, like Material or Cupertino widgets.
  • intl 0.17.0 — the backbone of the localization system; allows us to create and use our own localizations; used for formatting dates and numbers.

Now let’s look at the code for our starter app, which is pretty straightforward.

import 'package:flutter/material.dart';
import 'screens/hero_list.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Heroes of Computer Science',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HeroList(title: 'Heroes of Computer Science'),
    );
  }
}

Our root widget is a bread-and-butter MaterialApp, with a HeroList at its home route.

import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_i18n_2021/widgets/hero_card.dart';

class HeroList extends StatelessWidget {
  final String title;

  HeroList({this.title = ''});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            tooltip: 'Open settings',
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => Settings()),
              );
            },
          )
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Text('6 Hereos'),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  HeroCard(
                    name: 'Grace Hopper',
                    born: '9 December 1906',
                    bio: 'Devised theory of machine-independent '
                         'programming languages.',
                    imagePath: 'assets/images/grace_hopper.jpg',
                  ),
                  HeroCard(
                    name: 'Alan Turing',
                    born: '23 June 1912',
                    bio: 'Father of theoretical computer science & '
                        'artificial intelligence.',
                    imagePath: 'assets/images/alan_turing.jpg',
                  ),
              
             	  // ...
                  
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

HeroList mainly houses a ListView of parameterized HeroCards. HeroList’s Scaffold.appBar features an icon that links to the Settings screen. We’ll get to Settings when we build a manual language switcher in a future article update. Let’s move on to the HeroCard widget.

import 'package:flutter/material.dart';

class HeroCard extends StatelessWidget {
  final String name;
  final String born;
  final String bio;
  final String imagePath;

  final String placeholderImagePath = 'assets/images/placeholder.jpg';

  const HeroCard({
    Key key,
    this.name = '',
    this.born = '',
    this.bio = '',
    this.imagePath,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var theme = Theme.of(context);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(4.0),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(right: 8.0),
              child: ClipRRect(
                borderRadius: BorderRadius.circular(2),
                child: Image.asset(
                  imagePath ?? placeholderImagePath,
                  width: 100,
                  height: 100,
                ),
              ),
            ),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.only(top: 4),
                    child: Text(
                      name,
                      style: theme.textTheme.headline6,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 2, bottom: 4),
                    child: Text(
                      born.isEmpty ? '' : 'Born $born',
                      style: TextStyle(
                               fontSize: 12,
                               fontWeight: FontWeight.w300),
                    ),
                  ),
                  Text(
                    bio,
                    style: TextStyle(fontSize: 14),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

HeroCard displays the given image and string params in a nice Material Card widget and pretties everything up. Alright, let’s get to localizing this puppy!

🔗 Resource » You can get the code of the app up to this point from the start branch of our GitHub repo. The main branch has the fully localized app.

Installation & Setup

We can install our packages by adding a few lines to pubspec.yaml.

version: 1.0.0+1
 
 environment:
   sdk: ">=2.7.0 <3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
   flutter_localizations:
      sdk: flutter
   intl: ^0.17.0
 
 # ... 
 
 # The following section is specific to Flutter.
 flutter:
   generate: true
   uses-material-design: true

  # ...

After adding the highlighted lines above we can run flutter pub get from the command line to pull in our packages. The generate: true line is necessary for the automatic code generation the localization packages provide for us. We’ll go more into code generation business shortly. For now, do include the line; it really saves time.

Localization Configuration

With our packages installed, let’s add a l10n.yaml file to the root of our project. This file configures where our translation files will sit and the names of auto-generated dart files.

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

🔗 Resource » The official Internationalization User Guide covers many more options that can go in l10n.yaml to control the Flutter i18n code generator.

Adding Translation Files

Flutter localization uses ARB (Application Resource Bundle) files to house its translations by default. These are simple files written in JSON syntax. At the very least, we need a template file that corresponds to our default locale (English in our case). We specified that our template file will be lib/l10n/app_en.arb in our above configuration. So let’s create this housing directory and add our template translations file to it.

{
  "appTitle": "Heroes of Computer Science"
}

Of course, all these shenanigans wouldn’t make much sense if we couldn’t provide translations for other locales. We’ll add an Arabic translations file here. Feel free to add any language you like. We will touch on right-to-left (RTL) layouts when we update this article in the coming weeks, so if you’re interested in that you might want to stick to Arabic or another RTL language.

{
  "appTitle": "أبطال علوم الكمبيوتر"
}

We can add as many locale translations as we want. We just need to make sure that our files conform to our configured naming convention: lib/l10n/app_<locale>.arb

Configuring Our App

Let’s start telling our app about our anxious interest in i18n. We need to configure our main.dart file to use the Flutter localization packages.

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'screens/hero_list.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Heroes of Computer Science',
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        // 'en' is the language code. We could optionally provide a 
        // a country code as the second param, e.g. 
        // Locale('en', 'US'). If we do that, we may want to
        // provide an additional app_en_US.arb file for
        // region-specific translations.
        const Locale('en', ''),
        const Locale('ar', ''),
      ],
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HeroList(title: 'Heroes of Computer Science'),
    );
  }
}

After importing flutter_localizations.dart, we add the localizationsDelegates and supportedLocales props to the MaterialApp constructor. localizationsDelegates provide localizations to our app. The ones included above provide localizations for Flutter widgets, Material, and Cupertino, which have already been localized by the Flutter team.

For example, suppose we had a MaterialApp and called the showDatePicker() function somewhere within it. Supposing also that our operating system language is set to Arabic, we would see something like the following.

Note that we didn’t have to translate anything ourselves to get this. The date picker widget has already been localized by the Flutter team. We just need to wire up the correct delegates in our app constructor, as we did above. Big hats off to the Flutter team for this: what a time-saver!

🗒 Note » At the time of writing, flutter_localizations supports 78 languages.

🔗 Resource » The official Flutter documentation does a good job of explaining how the different parts, like delegates and the Localizations class, work together for their i18n/l10n.

The supportedLocales prop we provided to the MaterialApp constructor lists the languages our app supports. Flutter will only rebuild widgets in response to a locale change if the locale is in the supportedLocales list. We’ll get back to supportedLocales in a moment when we discuss locale resolution. Right now, let’s generate some code!

Automatic Code Generation

In order to use the translations in the ARB files in our Flutter app, we need to generate some Dart files that we import whenever we need the translations. To generate these files, just make sure you’ve followed the installation and setup steps up to this point and run the app. That’s right, just run the app. The code will automatically get generated, and if all went well, you should see the following files in your project directory:

  • .dart_tool/flutter_gen/gen_l10n/app_localizations.dart
  • .dart_tool/flutter_gen/gen_l10n/app_localizations_en.dart
  • .dart_tool/flutter_gen/gen_l10n/app_localizations_ar.dart

🗒 Note » If these files weren’t generated, make sure your Flutter app has no complication errors and check your debug console when you run the app.

Using Our AppLocalizations

Let’s make use of the newly generated code files to localize our app title.

import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'screens/hero_list.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateTitle: (context) { 
        return AppLocalizations.of(context).appTitle;
      },
      localizationsDelegates: [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en', ''),
        const Locale('ar', ''),
      ],
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),

      // remove home: HeroList(...)
      initialRoute: '/',
      routes: {
        '/': (context) {
          return HeroList(title: AppLocalizations.of(context).appTitle);
        },
        '/settings': (context) => Settings(),
      },
    );
  }
}

We import app_localizations.dart and add the auto-generated AppLocalizations.delegate to our delegate list. This provides us with the AppLocalizations widget, which we use to translate the app title and the HeroList title. The auto-generated appTitle property will contain the translation that matches the active locale, pulled from our app_<locale>.arb file.

✋🏽 Heads up » Due to loading order, our translations won’t be ready when we’re constructing our MaterialApp. So we use the onGenerateTitle and routes props, and their builder (context) {} functions to make sure that our translations are ready when we set our title strings.

Now, if we set our operating system language to Arabic and run our app, lo and behold!

Our title is now in Arabic. Moreover, notice how Flutter has laid out many of its widgets in a right-to-left direction automatically for us. Since Arabic is a right-to-left language, this saves us a ton of time! We’ll have to fix that padding to the left of the image in the HeroCards, and we’ll do that in an upcoming article update.

📖 Go deeper » The eagle-eyed reader may have noticed that AppLocalizations.of(context) looks a lot like calling an InheritedWidget. That’s because localization objects work a lot like InheritedWidgets. We cover this in more detail in our article, How to Internationalize a Flutter App Using intl and intl_translation.

That’s it for setup. We have the foundation for localizing our app now. One question that you might have at this point is, “how does Flutter decide what locale to use?” Let’s talk about that.

Locale Resolution

The locales we provided to MaterialApp(supportedLocales: [...]) are the only ones Flutter will use to determine the active locale when the app runs. To do this, Flutter uses three properties of a locale:

  • The language code, e.g. 'en'for English
  • The country code (optional), e.g. the US part in en_US
  • The script code (optional) —the letter set used, e.g. traditional (Hant) or simplified Chinese (Hans)

By default, Flutter will read the user’s preferred system locales and:

  1. Try to match the languageCode, scriptCode, and countryCode with one in supportedLocales. If that fails,
  2. Try to match the languageCode and scriptCode with one in supportedLocales. If that fails,
  3. Try to match the languageCode and countryCode with one in supportedLocales. If that fails,
  4. Try to match the languageCode with one in supportedLocales. If that fails,
  5. Try to match the countryCode with one in supportedLocales only when all preferred locales fail to match. If that fails,
  6. Return the first element of supportedLocales as a fallback.

So in our app, if the user’s iOS language is set to ar_SA, they would see our ar localizations (4. above) . If the user’s OS language is set to fr (French), they would see our en localizations (6. above). On Android, a user can have a list of preferred locales, not just one. This is covered by Flutter in the above resolution algorithm.

🔗 Resource » The above algorithm is covered in the official documentation of the supportedLocales property.

✋🏽 Heads up » If your app supports a locale with a country code, like fr_CA (Canadian French), you should provide a fallback without the country code, like fr.

Updating the iOS Project

The official Flutter documentation mentions the need to update the Info.plist directly in the iOS app bundle, adding our supported locales to it. If Info.plist isn’t updated, our iOS app might not work as expected. To make the update, we just need to open ios/Runner/Info.plist in any text editor and make sure the following entries are in there.

<key>CFBundleLocalizations</key>
<array>
	<string>en</string>
	<string>ar</string>
</array>

Getting the Active Locale

We sometimes need to know what the runtime locale is in our code. We can do this with the following snippet.

Locale activeLocale = Localizations.localeOf(context);

// If our active locale is fr_CA
debugPrint(activeLocale.languageCode); // => fr
debugPrint(activeLocale.countryCode);  // => CA

✋🏽 Heads up » Notice that we’re using Localizations , a widget built into Flutter, and not the auto-generated AppLocalizations.

Basic Translation Messages

We already covered basic translation messages when we added our appTitle message. However, let’s quickly go over the workflow for adding messages. We’ll translate the tooltip of our HeroList‘s app bar icon button next.

// ...

class HeroList extends StatelessWidget {
  final String title;

  HeroList({this.title = ''});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            tooltip: 'Open settings',
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => Settings()),
              );
            },
          )
        ],
      ),
      body: ...
  );
}

Let’s get that tooltip localized, shall we? First, we’ll add the relevant entries to our ARB files.

{
  "appTitle": "Heroes of Computer Science",
  "openSettings": "Open Settings"
}

{
  "appTitle": "أبطال علوم الكمبيوتر",
  "openSettings": "إفتح الإعدادات"
}

Next, let’s reload our app to regenerate our code files. This step is really important and forgetting it can lead to undue frustration. Note that this is a full app restart (), not a hot reload.

Now we can update our code to use our new localized message.

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

class HeroList extends StatelessWidget {
  final String title;

  HeroList({this.title = ''});

  @override
  Widget build(BuildContext context) {
    var t = AppLocalizations.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            tooltip: t.openSettings,
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => Settings()),
              );
            },
          )
        ],
      ),
      body: ...
  );
}

With this code in place, when we reload our app we should see the localized value of our tooltip in the Widget Inspector.

✋🏽 Heads up » You may often get error highlighting in your IDE after you add new translation messages. If you’ve reloaded your app, the error may be incorrect (you might be just fine). If you get a message saying that there are build errors, you can try to run the app anyway. If all is well and you see your new translations, then all is probably well. To make the error go away in the IDE, trying shutting your app down entirely and starting it up again.

🔗 Resource » Get the complete for our demo app from our GitHub repo.

Up, Up and Away

We hope you’re enjoying our in-progress guide to Flutter localization, and that you’re learning a thing or two. In the coming weeks, we’ll be adding sections covering interpolation, plurals, date formatting, and more. Stay tuned 😉

And if you’re looking to take your i18n game to the next level, check out Phrase. A professional localization platform with built-in ARB support for your Flutter apps, Phrase features a flexible API and CLI, and a beautiful web platform for your translators. With GitHub, GitLab, and Bitbucket sync, and Over the Air translations for mobile, Phrase does the heavy lifting in your localization pipeline to keep you focused on the code you love. Check out all of Phrase’s features and try it free for 14 days.

5 (100%) 59 votes
Comments
close

The Biggest Mistakes to Watch Out For in Localization

Download our FREE INFOGRAPHIC for a strong overview of the crucial mistakes you need to avoid to ensure your localization process has the best outcome possible.