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:
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!:
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!