I shipped my first Flutter app with hardcoded English strings everywhere. Login button? "Login". Error message? "Something went wrong". It worked fine — until a client in Saudi Arabia asked for Arabic support. That's when I spent three painful days ripping out every hardcoded string and retrofitting internationalization into an app that was never designed for it.
Don't make that mistake. Setting up i18n from the start takes about 30 minutes. Retrofitting it into an existing app takes days. This guide walks you through Flutter's official localization system — the one that uses ARB files and code generation — because it's the approach that scales, catches missing translations at compile time, and doesn't require any third-party packages beyond what Flutter already provides.
What You'll Learn
- Setting up flutter_localizations and intl from scratch
- Writing ARB translation files with plurals, gender, and placeholders
- Auto-generating type-safe localization code with
flutter gen-l10n - Formatting dates, numbers, and currencies per locale
- Supporting RTL languages (Arabic, Urdu, Hebrew)
- Runtime language switching without restarting the app
- Testing strategies for multi-language apps
Prerequisites
Flutter 3.27+ and Dart 3.6+. Familiarity with Flutter project structure and basic state management concepts. No prior i18n experience needed.
1. Why Internationalization Matters for Flutter Apps
Here's a number that should get your attention: 75% of the world's population doesn't speak English. If your app only supports English, you're ignoring three-quarters of potential users. And the markets growing fastest in mobile — South Asia, the Middle East, Latin America, Southeast Asia — are predominantly non-English.
The Business Case
Apps that support local languages see 2-3x higher conversion rates in non-English markets. We saw this first-hand with a fintech app we built for a client targeting Pakistan and the UAE. Adding Urdu and Arabic support increased daily active users by 140% within the first month. Users simply trust apps more when they see their own language.
Beyond downloads, there's the App Store optimization angle. The Google Play Store indexes your app's metadata in every language you support. An app listing in Arabic shows up in Arabic keyword searches that your English-only competitor can't reach. Same with the Apple App Store.
i18n vs l10n — What's the Difference?
These terms get thrown around interchangeably, but they're distinct:
- Internationalization (i18n) — The engineering work. Designing your app so it can support multiple languages. This means extracting strings, using locale-aware formatters, handling text direction changes.
- Localization (l10n) — The translation work. Actually writing the Arabic, Spanish, or Japanese versions of your strings. This is typically done by translators, not developers.
You do i18n once. You do l10n for every new language you add. Get the i18n architecture right and adding a new language is just dropping in a new ARB file.
2. Project Setup: flutter_localizations and intl
Flutter's localization system has two parts. The flutter_localizations package provides translated Material and Cupertino widget strings (button labels, date picker headers, dialog text). The intl package handles your custom app strings, plurals, and date/number formatting.
Adding Dependencies
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
dev_dependencies:
intl_utils: ^2.8.0 # Optional: IDE integration for ARB files
Run flutter pub get and you're set. No native configuration needed — this works on iOS, Android, web, desktop, all platforms.
The l10n.yaml Configuration File
Create a file called l10n.yaml in your project root. This tells Flutter's code generator where to find your ARB files and what to generate:
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: false
output-dir: lib/l10n/generated
nullable-getter: false
The key settings here: synthetic-package: false puts the generated code in your lib/ tree instead of a hidden .dart_tool folder, which makes it easier to inspect and debug. nullable-getter: false means AppLocalizations.of(context) returns a non-nullable value — no more null checks everywhere.
Configuring MaterialApp
Wire up the localization delegates in your MaterialApp:
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/generated/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// Optional: force a specific locale for testing
// locale: const Locale('ar'),
home: const HomeScreen(),
);
}
}
That localizationsDelegates getter is auto-generated and includes GlobalMaterialLocalizations, GlobalWidgetsLocalizations, GlobalCupertinoLocalizations, and your own AppLocalizations delegate. You don't need to list them manually anymore.
3. ARB Files: Your Translation Database
ARB stands for Application Resource Bundle. It's a JSON-based format that the Dart team chose for Flutter localization. Each locale gets its own ARB file — app_en.arb for English, app_ar.arb for Arabic, app_ur.arb for Urdu.
The Template File (English)
Create lib/l10n/app_en.arb — this is your source of truth:
{
"@@locale": "en",
"appTitle": "MyApp",
"@appTitle": {
"description": "The application title shown in the app bar"
},
"welcomeMessage": "Welcome back, {userName}!",
"@welcomeMessage": {
"description": "Greeting shown on the home screen",
"placeholders": {
"userName": {
"type": "String",
"example": "Shakil"
}
}
},
"loginButton": "Sign In",
"@loginButton": {
"description": "Label for the sign-in button on the login screen"
},
"emailHint": "Enter your email address",
"passwordHint": "Enter your password",
"forgotPassword": "Forgot your password?",
"noAccount": "Don't have an account? Sign up",
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Shows the number of items in the cart",
"placeholders": {
"count": {
"type": "int"
}
}
}
}
Notice the pattern: every key has a corresponding @key metadata entry with a description. This is optional but worth doing — translators use these descriptions to understand context. Without them, a translator seeing "save" won't know if it means "save to disk" or "save money."
Adding a Translation (Arabic)
Create lib/l10n/app_ar.arb:
{
"@@locale": "ar",
"appTitle": "تطبيقي",
"welcomeMessage": "!{userName} ،مرحباً بعودتك",
"loginButton": "تسجيل الدخول",
"emailHint": "أدخل بريدك الإلكتروني",
"passwordHint": "أدخل كلمة المرور",
"forgotPassword": "هل نسيت كلمة المرور؟",
"noAccount": "ليس لديك حساب؟ سجل الآن",
"itemCount": "{count, plural, =0{لا توجد عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}"
}
Arabic has six plural forms (zero, one, two, few, many, other) compared to English's two (one, other). The intl package handles this correctly based on CLDR plural rules. You don't need to know the rules — just provide the forms and Flutter picks the right one based on the count.
4. Code Generation with flutter gen-l10n
With your ARB files in place, run the code generator:
flutter gen-l10n
This creates a generated AppLocalizations class with type-safe getters for every string in your ARB files. If you misspell a key or forget a placeholder, you get a compile-time error instead of a runtime crash. That's the whole point of code generation.
What Gets Generated
The generator creates three files:
app_localizations.dart— The abstractAppLocalizationsclass with all your string gettersapp_localizations_en.dart— English implementationapp_localizations_ar.dart— Arabic implementation
Each getter is strongly typed. A string with a placeholder like {userName} becomes a method that requires a String argument:
// Generated code — don't edit this
abstract class AppLocalizations {
String get appTitle;
String welcomeMessage(String userName);
String get loginButton;
String itemCount(int count);
// ... more getters
}
If you add a new key to app_en.arb but forget to add it to app_ar.arb, the generator warns you. If you reference a key that doesn't exist, your IDE flags it immediately. No more runtime "key not found" errors.
Auto-Regeneration
Add generate: true to your pubspec.yaml to automatically regenerate localization code whenever you run flutter pub get or build:
# pubspec.yaml
flutter:
generate: true
This way you don't need to manually run flutter gen-l10n after every ARB file change. The build system handles it.
5. Using Translations in Your Widgets
Access your translated strings through AppLocalizations.of(context):
import 'l10n/generated/app_localizations.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.welcomeMessage('Shakil'),
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 32),
TextField(
decoration: InputDecoration(
hintText: l10n.emailHint,
prefixIcon: const Icon(Icons.email_outlined),
),
),
const SizedBox(height: 16),
TextField(
obscureText: true,
decoration: InputDecoration(
hintText: l10n.passwordHint,
prefixIcon: const Icon(Icons.lock_outline),
),
),
const SizedBox(height: 8),
Align(
alignment: AlignmentDirectional.centerEnd,
child: TextButton(
onPressed: () {},
child: Text(l10n.forgotPassword),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {},
child: Text(l10n.loginButton),
),
],
),
),
);
}
}
I like creating a shorthand extension so you don't need to type AppLocalizations.of(context) every time:
extension LocalizationExt on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
// Usage: context.l10n.loginButton
// Instead of: AppLocalizations.of(context).loginButton
Shorter, cleaner, and every developer on the team knows exactly what context.l10n means.
6. Plurals, Gender, and Select Messages
This is where most i18n guides stop — and where the real complexity lives. English has simple plural rules: one item, multiple items. Other languages? Not so simple. Arabic has six plural forms. Polish has four. Russian has three, and the rules for which form to use are based on the last two digits of the number.
Plural Messages
The ICU message format handles all of this. You define the forms, Flutter picks the right one:
// In app_en.arb
"unreadMessages": "{count, plural, =0{No unread messages} =1{1 unread message} other{{count} unread messages}}",
"@unreadMessages": {
"placeholders": {
"count": { "type": "int" }
}
}
// In app_ar.arb
"unreadMessages": "{count, plural, =0{لا توجد رسائل غير مقروءة} =1{رسالة واحدة غير مقروءة} =2{رسالتان غير مقروءتان} few{{count} رسائل غير مقروءة} many{{count} رسالة غير مقروءة} other{{count} رسالة غير مقروءة}}"
Gender-Specific Messages
Some languages conjugate differently based on gender. The select ICU format handles this:
"profileGreeting": "{gender, select, male{He updated his profile} female{She updated her profile} other{They updated their profile}}",
"@profileGreeting": {
"placeholders": {
"gender": { "type": "String" }
}
}
Nested Placeholders
You can combine plural and string placeholders in a single message:
"cartSummary": "{userName} has {count, plural, =0{an empty cart} =1{1 item in cart} other{{count} items in cart}}",
"@cartSummary": {
"placeholders": {
"userName": { "type": "String" },
"count": { "type": "int" }
}
}
// Usage in Dart:
Text(l10n.cartSummary('Shakil', 3))
// Output: "Shakil has 3 items in cart"
7. Date and Number Formatting by Locale
Dates and numbers are the sneaky localization trap. 3/4/2026 is March 4th in the US and April 3rd in Europe. 1,000.50 is a thousand and a half in English but looks like 1.000,50 in German. Get these wrong and your finance app shows the wrong amounts.
Date Formatting
import 'package:intl/intl.dart';
// Locale-aware date formatting
final now = DateTime.now();
final locale = Localizations.localeOf(context).toString();
// Short date: 4/1/2026 (en_US) or ١/٤/٢٠٢٦ (ar)
Text(DateFormat.yMd(locale).format(now));
// Full date: April 1, 2026 (en) or ١ أبريل ٢٠٢٦ (ar)
Text(DateFormat.yMMMMd(locale).format(now));
// Day + time: Tuesday 3:45 PM (en) or الثلاثاء ٣:٤٥ م (ar)
Text(DateFormat.EEEE(locale).add_jm().format(now));
// Relative time (custom helper):
String timeAgo(DateTime date, String locale) {
final diff = DateTime.now().difference(date);
if (diff.inMinutes < 1) return 'Just now';
if (diff.inHours < 1) return '${diff.inMinutes}m ago';
if (diff.inDays < 1) return '${diff.inHours}h ago';
return DateFormat.MMMd(locale).format(date);
}
Number and Currency Formatting
final locale = Localizations.localeOf(context).toString();
// Number: 1,234,567.89 (en) or ١٬٢٣٤٬٥٦٧٫٨٩ (ar)
Text(NumberFormat.decimalPattern(locale).format(1234567.89));
// Currency: $1,234.50 (en_US) or ١٬٢٣٤٫٥٠ ر.س (ar_SA)
Text(NumberFormat.currency(
locale: locale,
symbol: 'PKR ',
decimalDigits: 0,
).format(50000));
// Compact number: 1.2M (en) or ١٫٢ مليون (ar)
Text(NumberFormat.compact(locale: locale).format(1200000));
// Percentage: 75% (en) or ٧٥٪ (ar)
Text(NumberFormat.percentPattern(locale).format(0.75));
Always use these formatters instead of string interpolation. "$price PKR" won't adapt to locale. NumberFormat.currency() will.
8. Right-to-Left (RTL) Language Support
If you're supporting Arabic, Urdu, Hebrew, or Persian, RTL layout is non-negotiable. The good news: Flutter handles most of it automatically. The bad news: "most of it" isn't "all of it."
What Flutter Handles Automatically
- Text alignment flips (left-aligned becomes right-aligned)
Rowchildren reverse orderListViewscrolls correctly- Material widgets (AppBar, Drawer, navigation) mirror correctly
Scaffolddrawer position flips
What You Need to Fix Manually
Replace all EdgeInsets with EdgeInsetsDirectional:
// BAD: Won't flip for RTL
padding: const EdgeInsets.only(left: 16, right: 8),
// GOOD: Flips automatically for RTL
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
// BAD: Fixed alignment
alignment: Alignment.centerLeft,
// GOOD: Direction-aware alignment
alignment: AlignmentDirectional.centerStart,
// BAD: Fixed positioned
Positioned(left: 16, child: icon),
// GOOD: Direction-aware positioned
PositionedDirectional(start: 16, child: icon),
Icons That Should (and Shouldn't) Flip
Directional icons like back arrows, forward arrows, and list indicators should mirror in RTL. But logos, checkmarks, and non-directional icons should stay the same:
// Back arrow — should flip in RTL
Icon(
Directionality.of(context) == TextDirection.rtl
? Icons.arrow_forward
: Icons.arrow_back,
)
// Or use the directional variant:
const Icon(Icons.arrow_back) // Flutter auto-mirrors this in RTL
// Logo — should NOT flip
Directionality(
textDirection: TextDirection.ltr, // Force LTR for brand assets
child: Image.asset('assets/logo.png'),
)
Testing RTL Without Changing System Language
Force RTL in your MaterialApp for testing without changing your phone's system language:
MaterialApp(
locale: const Locale('ar'), // Force Arabic locale
// ... rest of config
)
9. Dynamic Locale Switching at Runtime
Users should be able to change the app language from settings without restarting. Here's a clean implementation using Riverpod and shared_preferences:
The Locale Provider
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
return LocaleNotifier();
});
class LocaleNotifier extends StateNotifier<Locale> {
LocaleNotifier() : super(const Locale('en')) {
_loadSavedLocale();
}
static const _key = 'app_locale';
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_key);
if (saved != null) {
state = Locale(saved);
} else {
// Fall back to device locale if no preference saved
final deviceLocale = PlatformDispatcher.instance.locale;
state = _isSupportedLocale(deviceLocale)
? deviceLocale
: const Locale('en');
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, locale.languageCode);
}
bool _isSupportedLocale(Locale locale) {
return ['en', 'ar', 'ur', 'es', 'fr'].contains(locale.languageCode);
}
}
Wiring It Into MaterialApp
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider);
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
);
}
}
Language Picker Widget
class LanguagePicker extends ConsumerWidget {
const LanguagePicker({super.key});
static const _languages = [
(locale: Locale('en'), name: 'English', flag: '🇺🇸'),
(locale: Locale('ar'), name: 'العربية', flag: '🇸🇦'),
(locale: Locale('ur'), name: 'اردو', flag: '🇵🇰'),
(locale: Locale('es'), name: 'Español', flag: '🇪🇸'),
(locale: Locale('fr'), name: 'Français', flag: '🇫🇷'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeProvider);
return ListView.builder(
shrinkWrap: true,
itemCount: _languages.length,
itemBuilder: (context, index) {
final lang = _languages[index];
final isSelected = currentLocale == lang.locale;
return ListTile(
leading: Text(lang.flag, style: const TextStyle(fontSize: 28)),
title: Text(lang.name),
trailing: isSelected ? const Icon(Icons.check, color: Colors.green) : null,
selected: isSelected,
onTap: () {
ref.read(localeProvider.notifier).setLocale(lang.locale);
Navigator.pop(context);
},
);
},
);
}
}
When a user taps a language, the locale changes, MaterialApp rebuilds, and the entire widget tree re-renders with the new translations. Instant. No restart.
10. Testing Your Localized App
Don't ship localized apps without testing them. I've seen production apps where Arabic text overflowed containers because nobody tested RTL layouts, and Spanish translations that were truncated because buttons were sized for English text.
Widget Testing with Specific Locales
testWidgets('Login screen renders correctly in Arabic', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ar'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LoginScreen(),
),
);
await tester.pumpAndSettle();
// Verify Arabic text is displayed
expect(find.text('تسجيل الدخول'), findsOneWidget);
// Verify RTL layout direction
final directionality = tester.widget<Directionality>(
find.byType(Directionality).first,
);
expect(directionality.textDirection, TextDirection.rtl);
});
Golden Tests for Visual Regression
Golden tests catch layout issues that string-matching tests miss. A button that looks fine in English might overflow in German (German words are 30% longer on average):
testWidgets('Login screen golden test - Arabic', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ar'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LoginScreen(),
),
);
await tester.pumpAndSettle();
await expectLater(
find.byType(LoginScreen),
matchesGoldenFile('goldens/login_screen_ar.png'),
);
});
Pseudo-Localization for Edge Cases
Before you have real translations, test with pseudo-localized strings. Replace every character with an accented version and add 30% more characters to simulate text expansion:
String pseudoLocalize(String text) {
const charMap = {
'a': 'à', 'b': 'ƀ', 'c': 'ç', 'd': 'đ', 'e': 'è',
'f': 'ƒ', 'g': 'ĝ', 'h': 'ĥ', 'i': 'ì', 'j': 'ĵ',
'k': 'ķ', 'l': 'ĺ', 'm': 'ɱ', 'n': 'ñ', 'o': 'ò',
'p': 'þ', 'q': 'ǫ', 'r': 'ŕ', 's': 'ŝ', 't': 'ţ',
'u': 'ù', 'v': 'ṽ', 'w': 'ŵ', 'x': 'ẋ', 'y': 'ý',
'z': 'ž',
};
final pseudo = text.split('').map((c) =>
charMap[c.toLowerCase()] ?? c
).join();
// Add 30% padding to simulate text expansion
final padding = '~' * (text.length * 0.3).ceil();
return '[$pseudo$padding]';
}
// "Sign In" becomes "[Ŝìĝñ Ìñ~~]"
If your UI looks correct with pseudo-localized strings (nothing overflows, nothing truncates), it'll handle real translations fine.
11. Production Checklist
Before you ship a localized app, run through this list. I've seen each of these cause production bugs:
- All hardcoded strings extracted. Search your codebase for string literals inside build methods. Every user-facing string should come from
AppLocalizations. - Date/number formatters use locale. No more
toString()on dates or numbers. UseDateFormatandNumberFormatwith the current locale. - RTL tested. Run the app in Arabic or Urdu and check every screen. Pay attention to padding, icons, and text alignment.
- Text overflow tested. German and French text is ~30% longer than English. Russian can be even longer. Make sure your containers handle expansion.
- Plurals verified. Test with 0, 1, 2, 5, 11, 21, 100 items. Arabic and Russian plural rules are complex — verify the right form shows for each count.
- App Store metadata localized. Title, description, screenshots, and keywords for each locale. This is separate from app i18n but equally important for discoverability.
- Fallback locale defined. If a user's device is set to a language you don't support, the app should fall back to English (or your primary language) instead of crashing.
- Font support verified. Your chosen font needs to support the character sets of all your target languages. Noto Sans covers virtually every script.
- No concatenated strings.
"Hello " + name + ", you have " + count + " items"is untranslatable. Use ICU messages with placeholders. - Translation keys are descriptive. Use
loginButtonnotbtn1. UsecartEmptyMessagenotmsg42. Translators need context.
| Aspect | Flutter Official (gen-l10n) | easy_localization | slang |
|---|---|---|---|
| Setup complexity | Medium | Low | Low |
| Type safety | Full (compile-time) | None (runtime strings) | Full (compile-time) |
| Plural/gender support | ICU format | ICU format | Custom syntax |
| File format | ARB (JSON-based) | JSON, YAML, CSV | YAML, JSON |
| Code generation | flutter gen-l10n | Not needed | Build runner |
| Hot reload support | Yes | Yes | Yes |
| RTL handling | Automatic | Automatic | Automatic |
| Maintained by | Flutter team | Community | Community |
| Recommendation | Production apps | Prototypes/MVPs | Dart-first teams |
My recommendation: use the official flutter gen-l10n approach for any app that might grow beyond a prototype. The type safety alone saves debugging hours. easy_localization is fine for quick projects where you want to move fast and don't care about compile-time checks.
Related Guides
- Flutter Clean Architecture: A Practical Guide
- BLoC vs Riverpod: Which State Management to Choose in 2026
- Flutter Testing Strategy: Unit, Widget, and Integration Tests
- Flutter App Security: A Complete Guide
- Flutter Dark Mode: Complete Implementation Guide
- Firebase Authentication in Flutter: Complete Guide 2026
- Flutter Performance: Optimization Techniques That Work
- Mastering Flutter Custom Widgets
- Mastering Flutter Layouts: Flex, Stack, and Custom Layouts
- GetX vs BLoC vs Riverpod: State Management Comparison 2026
Frequently Asked Questions
What's the difference between i18n and l10n in Flutter?
Internationalization (i18n) is designing your app so it can adapt to different languages and regions without code changes. Localization (l10n) is the actual translation work — creating ARB files with translated strings for each locale. Think of i18n as building the framework (locale-aware widgets, message lookup) and l10n as filling it with content. Flutter's official tooling uses 'l10n' in its naming (flutter_localizations, l10n.yaml) but handles both.
Should I use the intl package or flutter_localizations?
Use both — they serve different purposes. flutter_localizations provides locale-aware Material and Cupertino widgets (date pickers, dialogs, text direction). The intl package handles message extraction, plurals, gender, and number/date formatting. Your app needs flutter_localizations for framework-level locale support and intl for your custom translated strings. Since Flutter 3.0, the recommended setup uses both with ARB files and code generation via flutter gen-l10n.
How many languages should I support at launch?
Start with 2-3 languages maximum. English plus your primary target market language is enough for launch. Adding more later is simple if your i18n architecture is solid — it's just adding new ARB files. The real work is getting the architecture right first: extracting all hardcoded strings, handling plurals, testing RTL layouts. Don't translate into 15 languages if your app hasn't validated product-market fit.
Can I change the app language without restarting?
Yes. Wrap your MaterialApp in a state management solution (Provider, Riverpod, BLoC) that holds the current locale. When the user picks a new language, update the locale value and MaterialApp rebuilds with the new translations. No app restart needed. The example in this guide shows a complete implementation using Riverpod that persists the user's language choice across sessions using shared_preferences.
How do I handle RTL languages like Arabic and Urdu?
Flutter handles RTL automatically when you set the locale correctly. The Directionality widget flips layouts, padding, and alignment. But you still need to test: icons with directional meaning (arrows) should flip, but logos shouldn't. Use TextDirection.rtl in tests, check that Row/Column children reorder correctly, and verify that EdgeInsetsDirectional (not EdgeInsets) is used throughout your app.
What's the best way to organize ARB files for a large app?
Keep one ARB file per locale in a dedicated lib/l10n/ directory. Name them app_en.arb, app_ar.arb, app_ur.arb. For very large apps (500+ strings), use prefixed keys: auth_loginButton, settings_languageLabel. Don't create separate ARB files per feature — the gen-l10n tool expects one file per locale. Use key prefixes and keep a consistent naming convention across all locales.