Simplified Flutter Localization using a BuildContext extension

Source code on GitHub

If you plan to release your Flutter app to users around the world, sooner or later you'll need to localize it so that it supports multiple languages.

Luckily, Flutter already ships with useful APIs that make this task easier, along with documentation explaining the process in detail.

And as of Flutter 2.5, we can use the new skeleton app template which generates localizations by default using ARB files, so that we don't have to do all the setup steps by hand.

In this article, we will review the skeleton app template and make some improvements.

The result will be a simplified app template that uses a BuildContext extension to easily access localized strings inside our widgets.

Getting Started: The "Simplified" Skeleton App Template

For reference, we will start with a new Flutter app that was generated with the skeleton template:

flutter create -t skeleton localization_riverpod_flutter

This template comes with a settings screen that can be used to switch between light and dark mode.

But since we want to focus on localization, we can remove all the theming logic so that the root widget in our app is simplified:

import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'sample_feature/sample_item_details_view.dart'; import 'sample_feature/sample_item_list_view.dart'; class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( restorationScopeId: 'app', localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en', ''), ], onGenerateTitle: (BuildContext context) => AppLocalizations.of(context)!.appTitle, onGenerateRoute: ..., ); } }

This is a good starting point. But there is an improvement that we can make right off the bat. And that is to replace this code:

localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en', ''), ],

with this:

localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales,

Why is this valid code?

Well, the answer lies in the documentation for AppLocalizations.localizationsDelegates:

/// A list of this localizations delegate along with the default localizations /// delegates. /// /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. /// /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ];

As we can see, localizationsDelegates declares exactly the same list of delegates as the ones we were passing to our MaterialApp.

The bottom line is that we end up with this:

class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( restorationScopeId: 'app', localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, onGenerateTitle: (BuildContext context) => AppLocalizations.of(context)!.appTitle, onGenerateRoute: ..., ); } }

And now that our "simplified" setup is ready, let's see what else we can improve. 🔍

The problem with accessing AppLocalizations

If we look closely at our MaterialApp, we can spot this line:

onGenerateTitle: (BuildContext context) => AppLocalizations.of(context)!.appTitle,

This gives us a hint as to how we're supposed to use localized strings to our widgets, which is a multi-step process:

1. Add localized strings to the ARB file

First, we have to add localized strings to the src/localizations/app_en.arb file:

{ "appTitle": "localization_riverpod_flutter", "@appTitle": { "description": "The title of the application" }, "itemDetails": "Item Details", "@itemDetails": { "description": "Title of the Item Details Page" }, "moreInformationHere": "More Information Here", "@moreInformationHere": { "description": "Body of the Item Details Page" } }

2. Generate the AppLocalizations class

Then, we can run flutter gen-l10n, which will re-generate the app_localizations.dart file, using the configuration stored inside l10n.yaml (more on this later).

This way, the localized strings we need will be accessible as type-safe values inside the AppLocalizations class:

abstract class AppLocalizations { ... /// The title of the application /// /// In en, this message translates to: /// **'localization_riverpod_flutter'** String get appTitle; /// Title of the Item Details Page /// /// In en, this message translates to: /// **'Item Details'** String get itemDetails; /// Body of the Item Details Page /// /// In en, this message translates to: /// **'More Information Here'** String get moreInformationHere; }

3. Access the localized strings in our widgets

Finally, we can use the localized strings in our widgets. Example:

import 'package:flutter/material.dart'; // 1. Import this file import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class SampleItemDetailsView extends StatelessWidget { const SampleItemDetailsView({Key? key}) : super(key: key); static const routeName = '/sample_item'; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // 2. Access the localized string title: Text(AppLocalizations.of(context)!.itemDetails), ), body: Center( // 2. Access the localized string child: Text(AppLocalizations.of(context)!.moreInformationHere), ), ); } }

In practice, this last step causes some trouble leading to a poor development workflow.


For once, if we type AppLocalizations.of(context) and try to use the quick fix shortcut to add the missing import, VSCode refuses to show app_localizations.dart as one of the available options - or at least that's what happens when I try this:

VSCode doesn't show the import option for the app_localizations.dart file
VSCode doesn't show the import option for the app_localizations.dart file

And second, we are forced to use the ! operator and type AppLocalizations.of(context)!.something every time we want to access a localized string.

If we inspect the AppLocalizations class, we find that the static of method returns a nullable object:

abstract class AppLocalizations { ... // the return type is nullable static AppLocalizations? of(BuildContext context) { return Localizations.of<AppLocalizations>(context, AppLocalizations); } }

This makes sense, because the underlying Localizations widget is created as part of MaterialApp, and depending on what context we use, calling AppLocalizations.of(context) may or may not give us an object.

But in practice, all our widgets will be descendants of MaterialApp, so we shouldn't need to use the ! operator everywhere!

If you're confused about BuildContext and how it relates to accessing widgets, check this Decoding Flutter video about BuildContext.

There's got to be a better way.

The l10n.yaml file

What we said above is that we need to run flutter gen-l10n to generate the localizations for our project.

We can run flutter gen-l10n --help to discover all the available options.

Or we can edit the l10n.yaml file that was generated for us when we created the skeleton app template. By default, this file looks like this:

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

To solve our problem, we can add this line:

nullable-getter: false

Then, we can run flutter gen-l10n again. And if we open the generated AppLocalizations again, we find that something has changed:

abstract class AppLocalizations { ... // the return type is now non-nullable static AppLocalizations of(BuildContext context) { // note the ! at the end return Localizations.of<AppLocalizations>(context, AppLocalizations)!; } }

This means that when we access the localized strings in the widgets, we no longer need the ! operator:

Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context).itemDetails), ), body: Center( child: Text(AppLocalizations.of(context).moreInformationHere), ), )

However, typing AppLocalizations.of(context) every time is a bit long, and we can improve things further! 💪

BuildContext extension to the rescue

Thanks to the power of Dart extensions, we can write this little helper:

// app_localizations_context.dart import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; extension LocalizedBuildContext on BuildContext { AppLocalizations get loc => AppLocalizations.of(this); }

Then, we can update our widget class like this:

import 'package:flutter/material.dart'; // 1. new import import 'package:localization_riverpod_flutter/src/localization/app_localizations_context.dart'; class SampleItemDetailsView extends StatelessWidget { const SampleItemDetailsView({Key? key}) : super(key: key); static const routeName = '/sample_item'; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // 2. short-hand syntax title: Text(context.loc.itemDetails), ), body: Center( // 2. short-hand syntax child: Text(context.loc.moreInformationHere), ), ); } }

This is much better because we can now import app_localizations_context.dart rather than app_localizations.dart - and VSCode has no problem finding it!:

VSCode now shows the import option for our BuildContext extension
VSCode now shows the import option for our BuildContext extension

And we can type context.loc.itemDetails rather than AppLocalizations.of(context).itemDetails, which is shorter.

If we have dozens (or hundreds) of localized strings in our app, this amounts to a lot of developer happiness! 😀

Conclusion

We've now created a simplified app template that allows us to more easily localize strings in our widgets by using a handy BuildContext extension.

Along the way, we have also learned how to configure the l10n.yaml file to suit our needs.

You can find the full source code for this app template on this page on GitHub:

In the next article, we'll see how to access the AppLocalizations object outside our widgets (using Riverpod), so that we can use localized strings inside our business logic.


And for a more in-depth intro to internationalization and how to set it up for new and existing Flutter apps, see the official documentation:

Happy coding!

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.