Internationalization #
In the world of globally-scaled mobile app development, internationalization (abbreviated i18n) and localization (abbreviated l10n) aren’t just decorative app features, but a basic necessity. Internationalization is the process of designing and preparing your app to adapt to various languages and regional conventions without having to rewrite source code. Localization is the actual process of translating text, adjusting number, currency, and date formats, and adapting layout elements according to a specific region (locale).
Setting up an i18n system from the first day of app development is much easier and more efficient than having to do mass refactoring on hundreds of static strings already hardcoded in the widget tree when the app has become large.
Introduction: Why i18n & l10n Matter #
Flutter has been designed from the start with first-class localization support. Using Flutter’s official library has several main advantages over creating your own custom localization system:
- Industry Standard Compliance: Flutter adopts the ARB (Application Resource Bundle) format which is JSON-based and the ICU (International Components for Unicode) standard for pluralization and conditional localization.
- Type Safety: Flutter’s code generator (
gen-l10n) converts ARB files into strongly-typed Dart classes. You don’t call strings using string keys (like'welcome_message'), but directly call Dart methods (likel10n.welcomeMessage). If a typo occurs, the Dart compiler immediately detects it at build time. - Built-in Widget Support: The localization library automatically adjusts internal text on Flutter built-in components, like DatePickers, Calendars, TimePickers, and system dialogs.
Here’s a flow diagram of the translation, compilation, and consumption visualization process of localization files in your Flutter app lifecycle:
flowchart TD
subgraph InputFiles["1. Definition Files (Input)"]
ARB_ID["app_id.arb (Template)"]
ARB_EN["app_en.arb"]
ARB_AR["app_ar.arb"]
end
subgraph CodeGen["2. Code Generator (Compile-time)"]
L10N_Yaml["l10n.yaml Configuration"] --> GenTool["flutter gen-l10n Tool"]
ARB_ID --> GenTool
ARB_EN --> GenTool
ARB_AR --> GenTool
GenTool --> GenClass["AppLocalizations (Dart generated classes)"]
end
subgraph Runtime["3. Runtime Execution (Widget Tree)"]
GenClass -->|"Register Delegates"| MaterialApp["MaterialApp (Config)"]
MaterialApp -->|"Lookup via BuildContext"| Context["AppLocalizations.of(context)"]
Context -->|"Render Dynamic Text / RTL"| UI["UI Widgets (Text / Layout)"]
endProject Setup and Configuration #
To get started, you need to add localization support dependencies and enable the automatic code generation feature in the project configuration files.
1. Modifying pubspec.yaml #
Add the flutter_localizations library (available directly in the Flutter SDK) and the intl library for advanced number, currency, and date format handling. Don’t forget to enable the generate: true flag in the flutter configuration section.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.1 # Date formatting & pluralization library
flutter:
generate: true # Allows Flutter to do automatic code generation from ARB files
2. Creating the l10n.yaml Configuration File #
Create a new file named l10n.yaml in your project’s root folder (alongside pubspec.yaml). This file configures how the Flutter generator creates Dart classes from your ARB files.
# l10n.yaml
arb-dir: lib/l10n # The directory where you store .arb files
template-arb-file: app_id.arb # The main language reference/template file (Indonesian)
output-localization-file: app_localizations.dart
output-class: AppLocalizations # The main generated class name
Application Resource Bundle (ARB) - Syntax & Structure #
ARB files are standard JSON files containing translation key-value pairs. Each optional key can have additional metadata keys with the @ symbol prefix containing context descriptions for translators and dynamic variable declarations.
We’ll create three ARB files in the lib/l10n/ folder to support Indonesian, English, and Arabic.
1. Indonesian Template (app_id.arb) #
// lib/l10n/app_id.arb
{
"@@locale": "id",
"appTitle": "Toko Buku Kita",
"@appTitle": {
"description": "The main app title displayed in the app bar"
},
"welcomeMessage": "Selamat datang kembali, {username}!",
"@welcomeMessage": {
"description": "Welcome message on the home page",
"placeholders": {
"username": {
"type": "String",
"example": "Budi Santoso"
}
}
},
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{Buku belum tersedia} =1{Tersisa 1 buku terakhir!} other{Tersisa {jumlahBuku} buku lagi!}}",
"@notifikasiBukuTersedia": {
"description": "Book stock status with pluralization",
"placeholders": {
"jumlahBuku": {
"type": "int",
"format": "compact"
}
}
},
"informasiGender": "{gender, select, male{Bapak {nama}} female{Ibu {nama}} other{Sdr. {nama}}}",
"@informasiGender": {
"description": "Shows formal salutations based on gender",
"placeholders": {
"gender": {
"type": "String"
},
"nama": {
"type": "String"
}
}
}
}
2. English Translation (app_en.arb) #
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "Our Bookstore",
"welcomeMessage": "Welcome back, {username}!",
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{No books available} =1{Only 1 book left!} other{{jumlahBuku} books left!}}",
"informasiGender": "{gender, select, male{Mr. {nama}} female{Mrs. {nama}} other{{nama}}}"
}
3. Arabic Translation (app_ar.arb) #
In Arabic, the pluralization rules (ICU Plural rules) are much more complex than Indonesian/English because they distinguish zero (zero), one (one), two (two), few (few), many (many), and other (other) categories.
// lib/l10n/app_ar.arb
{
"@@locale": "ar",
"appTitle": "متجر الكتب الخاص بنا",
"welcomeMessage": "مرحباً بك مجدداً، {username}!",
"notifikasiBukuTersedia": "{jumlahBuku, plural, =0{لا توجد كتب متاحة} =1{متبقي كتاب واحد فقط!} =2{متبقي كتابين اثنين!} few{متبقي {jumlahBuku} كتب!} many{متبقي {jumlahBuku} كتاباً!} other{متبقي {jumlahBuku} كتاب!}}",
"informasiGender": "{gender, select, male{السيد {nama}} female{السيدة {nama}} other{{nama}}}"
}
After creating the files above, run the generator command in the terminal console to build the Dart classes:
flutter gen-l10n
This process also runs automatically every time you run flutter run or flutter build.
MaterialApp Configuration & Fallback Handling #
After the code generator successfully produces the AppLocalizations class, you must register the delegates and the supported language list into your MaterialApp.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Multi-language App',
// 1. Register Flutter's built-in localization delegates & our custom class
localizationsDelegates: const [
AppLocalizations.delegate, // Our generated class
GlobalMaterialLocalizations.delegate, // For Material widget internal text
GlobalWidgetsLocalizations.delegate, // Determines text direction (LTR/RTL)
GlobalCupertinoLocalizations.delegate, // For Cupertino (iOS) components
],
// 2. Determine which languages your app supports
supportedLocales: const [
Locale('id'), // Indonesian (Default)
Locale('en'), // English
Locale('ar'), // Arabic
],
// 3. Set the active locale. If null, the app automatically detects the device language
locale: const Locale('id'),
// 4. Fallback handler if the user's device language isn't supported
localeResolutionCallback: (Locale? deviceLocale, Iterable<Locale> supportedLocales) {
if (deviceLocale == null) return const Locale('id');
for (final locale in supportedLocales) {
if (locale.languageCode == deviceLocale.languageCode) {
return locale; // Use the device language if registered
}
}
// Return to Indonesian as the default fallback
return const Locale('id');
},
home: const HomeScreen(),
);
}
}
Using Translations in the Widget Tree #
To make calling localization code easier in the widget tree, you can create an extension method on BuildContext so you don’t have to write AppLocalizations.of(context)! repeatedly.
// lib/core/extensions/l10n_extension.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension AppLocalizationsX on BuildContext {
/// Opens a shortcut for calling translation data
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
Here’s an example implementation of using static, dynamic, pluralized, and conditional selection translations on a UI page:
// lib/presentation/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../../core/extensions/l10n_extension.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
// We leverage the context.l10n extension to cut boilerplate code
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.appTitle),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Dynamic text with String variables
Text(
context.l10n.welcomeMessage('Aditya Pratama'),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// 2. Text with complex pluralization formats
Text(context.l10n.notifikasiBukuTersedia(0)), // ID output: "Buku belum tersedia"
Text(context.l10n.notifikasiBukuTersedia(1)), // ID output: "Tersisa 1 buku terakhir!"
Text(context.l10n.notifikasiBukuTersedia(12)), // ID output: "Tersisa 12 buku lagi!"
const SizedBox(height: 16),
// 3. Text with conditional select logic
Text(context.l10n.informasiGender('male', 'Wibowo')), // Output: "Bapak Wibowo"
Text(context.l10n.informasiGender('female', 'Kartika')), // Output: "Ibu Kartika"
Text(context.l10n.informasiGender('other', 'Sanjaya')), // Output: "Sdr. Sanjaya"
],
),
),
);
}
}
Dynamic Locale Management at Runtime with Riverpod #
In production apps, users often want to manually change the app language through in-app settings, regardless of whatever system language is active on their devices. We’ll build a dynamic locale state management that persistently stores user language preferences using shared_preferences and exposes it using Riverpod.
1. Preference Storage Implementation #
// lib/core/storage/preferences_helper.dart
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesHelper {
static const String _keyLocale = 'app_locale';
final SharedPreferences _prefs;
PreferencesHelper(this._prefs);
/// Stores the active language code preference
Future<void> saveLocale(String? languageCode) async {
if (languageCode == null) {
await _prefs.remove(_keyLocale);
} else {
await _prefs.setString(_keyLocale, languageCode);
}
}
/// Reads the stored language code preference
String? getLocale() {
return _prefs.getString(_keyLocale);
}
}
2. State Notifier with Riverpod #
// lib/core/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../storage/preferences_helper.dart';
// Global provider for SharedPreferences
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('Please override this provider in main.dart');
});
// Provider for the storage helper
final preferencesHelperProvider = Provider<PreferencesHelper>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return PreferencesHelper(prefs);
});
// Main provider for monitoring the active Locale status
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale?>((ref) {
final helper = ref.watch(preferencesHelperProvider);
return LocaleNotifier(helper);
});
class LocaleNotifier extends StateNotifier<Locale?> {
final PreferencesHelper _helper;
LocaleNotifier(this._helper) : super(null) {
_loadStoredLocale();
}
void _loadStoredLocale() {
final languageCode = _helper.getLocale();
if (languageCode != null) {
state = Locale(languageCode);
} else {
state = null; // null means automatically following the device OS language
}
}
/// Changes the app language
Future<void> changeLocale(String languageCode) async {
await _helper.saveLocale(languageCode);
state = Locale(languageCode);
}
/// Resets back to following the operating system default language
Future<void> resetToSystemLanguage() async {
await _helper.saveLocale(null);
state = null;
}
}
3. Language Selector Dropdown UI Integration #
// lib/presentation/widgets/language_selector_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/locale_provider.dart';
class LanguageSelectorWidget extends ConsumerWidget {
const LanguageSelectorWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final Locale? activeLocale = ref.watch(localeProvider);
return DropdownButton<String>(
value: activeLocale?.languageCode ?? 'id',
icon: const Icon(Icons.language),
underline: Container(height: 2, color: Colors.blueAccent),
onChanged: (String? newCode) {
if (newCode != null) {
ref.read(localeProvider.notifier).changeLocale(newCode);
}
},
items: const [
DropdownMenuItem(
value: 'id',
child: Text('🇮🇩 Bahasa Indonesia'),
),
DropdownMenuItem(
value: 'en',
child: Text('🇬🇧 English'),
),
DropdownMenuItem(
value: 'ar',
child: Text('🇸🇦 العربية (RTL)'),
),
],
);
}
}
Right-to-Left (RTL) Layout Support #
Text reading direction isn’t always from left to right (Left-to-Right / LTR). Several major world languages like Arabic (ar), Hebrew (he), and Persian (fa) use right-to-left (Right-to-Left / RTL) writing directions.
Flutter has an extraordinary architectural advantage in handling RTL automatically. When the app detects the active locale changed to Arabic, all horizontal coordinate axes and reorderable layouts are automatically reversed in direction by the Flutter render engine. However, as a developer, you must obey widget writing rules so you don’t break that symmetrical layout.
1. Use Logical Coordinates Instead of Absolute Coordinates #
Never use absolute-type padding or margin values (left and right) for dynamic components. Use logical coordinate axes (start and end).
// ANTI-PATTERN: Padding won't change position when the layout is flipped to RTL
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 8.0),
child: const Text('Content Index'),
)
// CORRECT: start means left in LTR, and automatically becomes right in RTL
Padding(
padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0),
child: const Text('Content Index'),
)
This logical coordinate rule also applies to alignment and border radius properties:
- Use
AlignmentDirectional.centerStartinstead ofAlignment.centerLeft. - Use
BorderRadiusDirectional.only(topStart: Radius.circular(8))instead ofBorderRadius.only(topLeft: Radius.circular(8)).
2. Icon Direction Handling (RTL-aware Icons) #
Some navigation indicator icons (like back arrows or forward arrows) must be reversed in RTL mode because users read from right to left. You must use Flutter’s built-in icons that have automatic direction adaptation:
// Use icons ending in _directional so they automatically flip direction when RTL is active
Icon(Icons.arrow_back_ios_new_outlined) // Automatically becomes a right arrow in RTL
// For static icons depicting objects (like cameras, settings, share),
// their direction stays the same and doesn't need to be flipped.
Number, Currency, and Date Formatting with the intl Library #
Each region has distinctive decimal number display formats, currency symbols, and month naming conventions. The intl package provides type-safe APIs to standardize these displays according to the user’s locale.
Here’s a helper class to make formatting this data easier:
// lib/core/utils/format_helper.dart
import 'package:intl/intl.dart';
class FormatHelper {
/// Formats regular (decimal) numbers according to the active locale
/// id: 1.250.000,75
/// en: 1,250,000.75
static String formatNumber(double value, String locale) {
return NumberFormat.decimalPattern(locale).format(value);
}
/// Formats regionally standardized currency
/// id: Rp150.000
/// en: $10.00
static String formatCurrency(double value, {required String currencyCode, required String locale}) {
return NumberFormat.simpleCurrency(
locale: locale,
name: currencyCode,
decimalDigits: currencyCode == 'IDR' ? 0 : 2,
).format(value);
}
/// Formats long dates
/// id: 16 Juni 2026
/// en: June 16, 2026
/// ar: ١٦ يونيو ٢٠٢٦
static String formatLongDate(DateTime date, String locale) {
return DateFormat.yMMMMd(locale).format(date);
}
/// Converts relative time (human readable)
/// Example output: "3 minutes ago" or "previously"
static String formatRelativeTime(DateTime targetTime, String locale) {
final DateTime now = DateTime.now();
final Duration difference = now.difference(targetTime);
if (difference.inMinutes < 1) {
return locale == 'id' ? 'Baru saja' : 'Just now';
} else if (difference.inHours < 1) {
return locale == 'id'
? '${difference.inMinutes} menit yang lalu'
: '${difference.inMinutes} minutes ago';
} else if (difference.inDays < 1) {
return locale == 'id'
? '${difference.inHours} jam yang lalu'
: '${difference.inHours} hours ago';
} else {
return formatLongDate(targetTime, locale);
}
}
}
Summary #
- Type Safety: Leverage the official
flutter gen-l10ngenerator to convert ARB JSON files into type-safeAppLocalizationsDart classes to prevent string key typing errors.- ICU Pluralization: Use the ICU pluralization format in ARB files to handle noun quantity category differences, especially in Arabic which has 6 plural form variations.
- Runtime Language Switching: Persistently store user language preferences in
SharedPreferencesand distribute the locale toMaterialAppthrough Riverpod to support instant language changes without app restarts.- RTL Design Principles: Always use logical direction properties like
EdgeInsetsDirectionalandAlignmentDirectionalso app layouts dynamically adapt when users switch languages to right-to-left writing modes.- intl Standardization: Use the
intllibrary to automatically format decimal, currency, and date visuals based on local locale representations.
← Previous: Push Notification & Deep Link Next: Accessibility →