Flutter — Depth to Localization

Enes Akbal
11 min readFeb 1, 2024

--

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 of count followed by "kangaroos". For example, if count 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
}
}
}
}

Click for more information about number formats.

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

--

--