Flutter — Depth to Localization
Localization, also known as l10n, is a very important part of making apps. It helps apps to be used by more people around the world by adding different languages and local features.
In this article, I attempted to explain how to do localization dynamically.
Dependencies And Configurations
Let’s start by adding the necessary packages and configurations to the project.
# pubspec.yaml file must be like below
dependencies:
flutter:
sdk: flutter
flutter_localizations: # required for localization
sdk: flutter
intl: ^0.18.0 # for localization
provider: ^6.1.1 # for change locale
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
generate: true # required for generating localization files
.arb Files
In this example, I’m going to add three languages: English, Turkish, and German. We’ll use .arb files for this. Think of these as files for translations. For each language, we’ll create a separate .arb file. Inside the .arb files, we need to create a key named @@locale
and assign which language it corresponds to. This way, we define which .arb file should be used for each language.
Let’s create .arb files
{
"@@locale": "en",
"helloWorld": "hello world"
}
{
"@@locale": "tr",
"helloWorld":"Merhaba Dünya"
}
{
"@@locale": "de",
"helloWorld": "Hallo Welt"
}
l10n.yaml
The l10n.yaml
file is a configuration file used in Flutter apps for setting up localization options. The term 'l10n' stands for 'localization', where 10 represents the number of letters between the first 'l' and the last 'n' in the word 'localization'.
Let’s create l10n.yaml
arb-dir: lib/l10n/arb
template-arb-file: app_de.arb
output-localization-file: app_localizations.dart
nullable-getter: false
untranslated-messages-file: untranslated_messages.json
Key Components
- arb-dir: Specifies the directory where the ARB (Application Resource Bundle) files are located.
- template-arb-file: Specifies the name of the template ARB file.
- output-localization-file: Specifies the name of the output file that will contain the generated localizations.
- nullable-getter: Specifies whether the generated getter methods should be nullable or not.
- untranslated-messages-file: Specifies the name of the file that will contain the untranslated messages.
There are other key components too. You can find more details here.
Generated Files
When run the project, generates the localization files in the .dart_tool/flutter_gen/gen_l10n
directory.
.dart_tool/flutter_gen/gen_l10n
directory contains automatically generated Dart files for each language supported in your project. These files are used to manage and provide localized content within your app.
Before using these files, we need to make some changes in the main.dart file.
- localizationDelegates: This is a list of delegates that will produce collections of localized values.
- supportedLocales: This is a list of locales that the application has been localized for. It's used to select the locale for the app's
Localizations
widget.
In summary, these two lines are configuring the MaterialApp
widget to support localization using the AppLocalizations
class.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:l10n_example/views/home_view.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'L10n Example',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: HelloWorld(),
);
}
}
Example Usage
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).helloWorld),
],
),
),
);
}
}
AppLocalizationX Extension
We can define each time as AppLocalizations.of(context), but this can reduce the readability of the code. Therefore, we will write an extension on BuildContext. This will make our code more readable.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
export 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
Example Usage with extension
import 'package:flutter/material.dart';
import 'package:l10n_example/l10n/l10n.dart';
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n; //* easiyl usage
return Scaffold(
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.helloWorld), //* returns "Hello World"
],
),
),
);
}
}
Change Locale
We will use provider
package for change locale.
Let’s create locale_provider.dart
file.
import 'package:flutter/material.dart';
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('en');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}
Wrap MaterialApp with ChangeNotifierProvider
widget
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LocaleProvider(),
child: Builder(
builder: (context) {
final provider = Provider.of<LocaleProvider>(context);
return MaterialApp(
title: 'L10n Example',
locale: provider.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeView(),
);
},
),
);
}
}
Let’s create a Custom App Bar for change locale
Firstly we will add homeAppBarTitle
key to .arb files. The "@homeAppBarTitle"
key is a special key that provides metadata about the homeAppBarTitle
key. This metadata includes a description of the key and placeholders that can be filled in when the text is displayed.
The description
field is optional parameter for explain the actual key.
{
"@@locale": "en",
"homeAppBarTitle": "Internationalization",
"@homeAppBarTitle": {
"description": "Purpose of the App"
}
}
{
"@@locale": "tr",
"homeAppBarTitle": "Uluslararasılaştırma",
"@homeAppBarTitle": {
"description": "Uygulamanın Amacı"
}
}
{
"@@locale": "de",
"homeAppBarTitle": "Internationalisierung",
"@homeAppBarTitle": {
"description": "Zweck der App"
},
}
And let’s create CustomAppBar
widget.
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
const CustomAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AppBar(
title: Text(
l10n.homeAppBarTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.white),
),
centerTitle: false,
backgroundColor: Theme.of(context).colorScheme.primary,
actions: [
IconButton(
icon: const Icon(Icons.language, color: Colors.white),
onPressed: () {
final locale = Localizations.localeOf(context);
final provider = Provider.of<LocaleProvider>(context, listen: false);
//* switch between locales
switch (locale.languageCode) {
case 'en':
provider.setLocale(const Locale('tr'));
break;
case 'tr':
provider.setLocale(const Locale('de'));
break;
case 'de':
provider.setLocale(const Locale('en'));
break;
default:
provider.setLocale(const Locale('en'));
break;
}
},
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
Simple Text
Configuration
"simpleTextTitle": "Simple Text",
"simpleTextContent": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"
Usage
class SimpleTextWidget extends StatelessWidget {
const SimpleTextWidget({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return CustomText(
title: l10n.simpleTextTitle,
content: Text(l10n.simpleTextContent),
);
}
}
Calendar Date Picker
Configuration
"calendarDatePickerTitle": "Calendar Date Picker",
"calendarDatePickerButton": "Pick Date",
"calendarDatePickerNoSelected": "No date selected"
Usage
class CalendarDatePickerWidget extends StatefulWidget {
const CalendarDatePickerWidget({super.key});
@override
State<CalendarDatePickerWidget> createState() => _CalendarDatePickerWidgetState();
}
class _CalendarDatePickerWidgetState extends State<CalendarDatePickerWidget> {
//* A value notifier to listen to the selected date
late final ValueNotifier<DateTime?> selectedDate;
@override
void initState() {
selectedDate = ValueNotifier<DateTime?>(null);
super.initState();
}
@override
void dispose() {
selectedDate.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return CustomText(
title: l10n.calendarDatePickerTitle,
content: Row(
children: [
//* CalendarDatePicker
TextButton(
onPressed: () async {
selectedDate.value = await showDatePicker(
context: context,
currentDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime(2100),
initialDatePickerMode: DatePickerMode.day,
) ??
DateTime.now();
},
child: Text(l10n.calendarDatePickerButton),
),
const Spacer(),
//* Selected Date
ValueListenableBuilder<DateTime?>(
valueListenable: selectedDate,
builder: (_, selectedDate, __) {
return Builder(
builder: (context) {
if (selectedDate != null) {
final locale = Localizations.localeOf(context);
return Text(
DateFormat.yMMMEd(locale.languageCode).format(selectedDate),
style: const TextStyle(fontSize: 14),
);
}
return Text(
l10n.calendarDatePickerNoSelected,
style: const TextStyle(fontSize: 14),
);
},
);
},
),
],
),
);
}
}
Basic Placeholder Text
Configuration
"basicPlaceholderTitle": "Basic Placeholder Text",
"basicPlaceholderContent": "Hello, my name is {name}. I am {age} years old with {experience} years of experience with Flutter. Born on {birthDate} in {birthPlace}",
"@basicPlaceholderContent": {
"description": "A message with name, age, experience, birthDate and birthPlace parameters",
"placeholders": {
"name": {
"type": "String",
"example": "Enes",
"description": "Name of the user"
},
"age": {
"type": "int",
"example": "23",
"description": "Age of the user"
},
"experience": {
"type": "double",
"example": "2.5",
"description": "Experience of the user"
},
"birthDate": {
"type": "DateTime",
"format": "yMMMEd",
"example": "1996-08-15",
"description": "Birth date of the user"
},
"birthPlace": {
"type": "String",
"example": "Istanbul, Türkiye",
"description": "Birth place of the user"
}
}
}
The "@basicPlaceholderContent"
key provides metadata about the basicPlaceholderContent
key. The placeholders are defined in the placeholders
object. Each placeholder has a type
, example
, and description
. The type
is the data type of the placeholder, the example
is an example value for the placeholder, and the description
is a description of what the placeholder represents.
For example, the name
placeholder has a type of String
, an example value of Enes
, and a description of Name of the user
. This means that wherever {name}
appears in the basicPlaceholderContent
text, it will be replaced with a user's name when the text is displayed.
The birthDate
placeholder is a bit special because it has an additional format
field. This field specifies the format that the date should be displayed in. The yMMMEd
format means year, abbreviated month, and day. For example, 1996-08-15
would be displayed as Aug 15, 1996
.
Usage
import 'package:flutter/material.dart';
import 'package:l10n_example/l10n/l10n.dart';
import 'package:l10n_example/widgets/_custom_text_widget.dart';
class BasicPlaceholderTextWidget extends StatelessWidget {
const BasicPlaceholderTextWidget({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
//* Text with parameters
return CustomText(
title: l10n.basicPlaceholderTitle,
content: Text(
l10n.basicPlaceholderContent(
'Enes', //* name
22, //* age
2.5, //* experience
DateTime(2001, 07, 29), //* birth date
'Istanbul, Türkiye', //* birth place
),
),
);
}
}
Plural Text
Configuration
"nKangaroosTitle": "Plural Text",
"nKangaroosContent": "{count, plural, =0{no kangaroos} =1{1 kangaroo} other{{count} kangaroos}}",
"@nKangaroosContent": {
"description": "A plural message for kangaroos",
"placeholders": {
"count": {
"type": "num",
"format": "compact",
"example": "0",
"description": "Number of kangaroos"
}
}
}
The key "nKangaroosContent"
has a more complex value. This value is a string that includes a placeholder {count}
and a special syntax for handling pluralization. The {count, plural, =0{no kangaroos} =1{1 kangaroo} other{{count} kangaroos}}
syntax is a way to handle different plural forms based on the count
value.
- If
count
is 0, the text will be "no kangaroos". - If
count
is 1, the text will be "1 kangaroo". - For any other value of
count
, the text will be the value ofcount
followed by "kangaroos". For example, ifcount
is 5, the text will be "5 kangaroos".
The "@nKangaroosContent"
key provides metadata about the nKangaroosContent
key. It includes a description
of the key and a placeholders
object that defines the count
placeholder. The count
placeholder has a type
of num
, a format
of compact
, an example
value of 0
, and a description
of "Number of kangaroos".
The type
is the data type of the placeholder, the format
is the format that the number should be displayed in, the example
is an example value for the placeholder, and the description
is a description of what the placeholder represents.
Usage
import 'package:flutter/material.dart';
import 'package:l10n_example/l10n/l10n.dart';
import 'package:l10n_example/widgets/_custom_text_widget.dart';
class TextWithPluralWidget extends StatelessWidget {
const TextWithPluralWidget({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
//* Text with plural
return CustomText(
title: l10n.nKangaroosTitle,
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(l10n.nKangaroosContent(0)), //* Returns '0 kangaroos'
Text(l10n.nKangaroosContent(1)), //* Returns '1 kangaroo'
Text(l10n.nKangaroosContent(12)), //* Returns '12 kangaroos'
],
),
);
}
}
Select Text
Configuration
"pronounTitle": "Select Gender Text",
"pronounContent": "{gender, select, male{he} female{she} other{they}}",
"@pronounContent": {
"description": "A gendered message",
"placeholders": {
"gender": {
"type": "String",
"example": "he"
}
}
}
The select
statement allows you to choose different texts based on a specific condition. This statement is particularly useful for managing language options based on variables like gender. Its basic usage is as follows:
"{placeholder, select, condition{message} ... other{otherMessage}}"
This structure returns the text corresponding to a case
based on the specified placeholder
value. If no case
matches, it uses the text defined under other
.
Usage
import 'package:flutter/material.dart';
import 'package:l10n_example/l10n/l10n.dart';
import 'package:l10n_example/widgets/_custom_text_widget.dart';
class TextWithSelectWidget extends StatelessWidget {
const TextWithSelectWidget({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
//* Text with Select
return CustomText(
title: l10n.pronounTitle,
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(l10n.pronounContent('male')), //* Returns 'he'
Text(l10n.pronounContent('female')), //* Returns 'she'
Text(l10n.pronounContent('other')), //* Returns 'they'
Text(l10n.pronounContent('wrong key')), //* Returns 'they'
],
),
);
}
}
Number and Currencies
Configuration
"numberAndCurrenciesTitle": "Number and Currencies",
"simpleCurrencyContent": "The price of the product is {currency1}",
"@simpleCurrencyContent": {
"description": "A message with a formatted int|double parameter",
"placeholders": {
"currency1": {
"type": "double",
"format": "simpleCurrency",
"description": "Price of the product",
"example": "1000.0",
"optionalParameters": {
"decimalDigits": 2
}
}
}
},
"compactLongContent": "The population of {country} is {compactLong}",
"@compactLongContent": {
"description": "A message with a formatted double parameter",
"placeholders": {
"compactLong": {
"type": "double",
"format": "compactLong",
"description": "Population of the country"
},
"country": {
"type": "String",
"example": "Turkey",
"description": "Name of the country"
}
}
},
"percentContent": "{country} accounts for approximately {percent} of the world's population",
"@percentContent": {
"description": "A message with a formatted double parameter",
"placeholders": {
"percent": {
"type": "double",
"format": "decimalPercentPattern",
"description": "Percentage of the world's population",
"example": "0.01"
},
"country": {
"type": "String",
"example": "Turkey",
"description": "Name of the country"
}
}
}
In different places, numbers and money are shown in different ways. Flutter uses a tool called flutter_localizations
and a class namedNumberFormat
from the intl
package to change how numbers look based on where you are and how you want them to look.
Types like int
, double
, and number
can use different setups from NumberFormat
to show numbers in various ways. This helps make your app work better for people from different parts of the world.
In the table given, some NumberFormat
constructors have stars next to them. This means they can use extra options. You can set these options in the optionalParameters
object for a placeholder. For example, if you want to set the decimalDigits parameter for the compactCurrency format, make like this:
"numberOfDataPoints": "Number of data points: {value}",
"@numberOfDataPoints": {
"description": "A message with a formatted int parameter",
"placeholders": {
"value": {
"type": "int",
"format": "compactCurrency",
"optionalParameters": {
"decimalDigits": 2
}
}
}
}
Conclusion
In this article, I tried to explain how to do localization in Flutter. You can access the source codes of the project from the repository below.
Thank you for reading.
If you liked my article, don’t forget to clap and share it.
Github Repository
Ref