← Back to Blog

Flutter Localization Complete Guide 2026: From Setup to Production

flutterlocalizationi18nguidearbrtltutorial

Flutter Localization Complete Guide 2026: From Setup to Production

Flutter localization lets you build apps that adapt to different languages, regions, and cultures. Whether you are adding your first translation or scaling to dozens of locales, this guide covers every step from initial setup to production deployment with practical examples and proven patterns.

Why Localize Your Flutter App

Expanding to new markets is one of the fastest ways to grow your app's user base. Users are far more likely to install and engage with an app in their native language. The Google Play Store and Apple App Store both rank localized apps higher in local search results. Flutter's built-in localization system makes this straightforward.

Benefits of localizing your Flutter app:

  • Larger audience: Reach users who prefer or require non-English interfaces
  • Higher retention: Users stay longer in apps that speak their language
  • Better store ranking: App stores favor localized metadata and content
  • Legal compliance: Some markets require apps to support the local language
  • Competitive edge: Many apps skip localization, leaving an opportunity

Setting Up Flutter Localization

Step 1: Add Dependencies

Add the localization packages to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any

flutter:
  generate: true

The flutter_localizations package provides built-in translations for Material and Cupertino widgets. The intl package handles date, number, and message formatting.

Step 2: Configure l10n.yaml

Create an l10n.yaml file in your project root:

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: true
nullable-getter: false

This tells Flutter where to find your translation files and how to generate the localization classes.

Step 3: Create ARB Files

ARB (Application Resource Bundle) is the standard format for Flutter translations. Create your template file at lib/l10n/app_en.arb:

{
  "@@locale": "en",
  "appTitle": "My App",
  "@appTitle": {
    "description": "The title of the application"
  },
  "welcomeMessage": "Welcome, {userName}!",
  "@welcomeMessage": {
    "description": "Welcome message shown on the home screen",
    "placeholders": {
      "userName": {
        "type": "String",
        "example": "John"
      }
    }
  },
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "description": "Shows the number of items",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

Then create translation files for each supported locale. For example, lib/l10n/app_ar.arb:

{
  "@@locale": "ar",
  "appTitle": "تطبيقي",
  "welcomeMessage": "!{userName} مرحبًا",
  "itemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}"
}

Step 4: Configure MaterialApp

Wire the localizations into your MaterialApp:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomeScreen(),
    );
  }
}

Step 5: Use Translations in Widgets

Access translations through AppLocalizations.of(context):

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(l10n.welcomeMessage('Flutter Developer')),
            Text(l10n.itemCount(42)),
          ],
        ),
      ),
    );
  }
}

Run flutter gen-l10n or let the build system generate the localization files automatically.

Working with ARB Files

Placeholders

Placeholders let you insert dynamic values into translated strings:

{
  "greeting": "Hello, {name}! You have {count} new messages.",
  "@greeting": {
    "placeholders": {
      "name": { "type": "String" },
      "count": { "type": "int" }
    }
  }
}

Pluralization

Flutter supports ICU plural syntax for handling different quantity forms:

{
  "emailCount": "{count, plural, =0{No emails} =1{1 email} other{{count} emails}}",
  "@emailCount": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}

Arabic, for example, has six plural forms (zero, one, two, few, many, other), so provide all of them:

{
  "emailCount": "{count, plural, =0{لا رسائل} =1{رسالة واحدة} =2{رسالتان} few{{count} رسائل} many{{count} رسالة} other{{count} رسالة}}"
}

Select Messages

Use select for gender-specific or category-based text:

{
  "userRole": "{role, select, admin{Administrator} editor{Editor} viewer{Viewer} other{User}}",
  "@userRole": {
    "placeholders": {
      "role": { "type": "String" }
    }
  }
}

Number and Date Formatting

Use the intl package for locale-aware formatting:

import 'package:intl/intl.dart';

class FormattedDataExample extends StatelessWidget {
  const FormattedDataExample({super.key});

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context).toString();

    final price = NumberFormat.currency(
      locale: locale,
      symbol: '\$',
    ).format(1234.56);

    final date = DateFormat.yMMMMd(locale).format(DateTime.now());

    return Column(
      children: [
        Text(price),   // $1,234.56 in en, 1.234,56 $ in de
        Text(date),    // March 3, 2026 in en, 3 مارس 2026 in ar
      ],
    );
  }
}

Right-to-Left (RTL) Support

RTL languages like Arabic, Hebrew, and Persian require layout mirroring. Flutter handles most RTL adaptation automatically, but you should follow these patterns.

Use Directional Widgets

Replace fixed-direction properties with directional equivalents:

// Instead of EdgeInsets, use EdgeInsetsDirectional
Padding(
  padding: EdgeInsetsDirectional.only(start: 16, end: 8),
  child: Text(l10n.menuItem),
)

// Instead of Alignment, use AlignmentDirectional
Align(
  alignment: AlignmentDirectional.centerStart,
  child: Text(l10n.label),
)

// Instead of BorderRadius, use BorderRadiusDirectional
Container(
  decoration: BoxDecoration(
    borderRadius: BorderRadiusDirectional.only(
      topStart: Radius.circular(12),
    ),
  ),
)

Check Text Direction

When you need conditional logic based on layout direction:

final isRtl = Directionality.of(context) == TextDirection.rtl;

Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward)

Test RTL Layouts

Force RTL in tests or debug mode:

Directionality(
  textDirection: TextDirection.rtl,
  child: MyWidget(),
)

Changing Language at Runtime

Let users switch languages without restarting the app:

class LocaleProvider extends ChangeNotifier {
  Locale? _locale;

  Locale? get locale => _locale;

  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }

  void clearLocale() {
    _locale = null;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => LocaleProvider(),
      child: Consumer<LocaleProvider>(
        builder: (context, provider, _) {
          return MaterialApp(
            locale: provider.locale,
            localizationsDelegates: AppLocalizations.localizationsDelegates,
            supportedLocales: AppLocalizations.supportedLocales,
            home: const HomeScreen(),
          );
        },
      ),
    );
  }
}

Persist Language Selection

Save the user's language choice using SharedPreferences:

import 'package:shared_preferences/shared_preferences.dart';

class LocaleService {
  static const _key = 'selected_locale';

  static Future<void> saveLocale(String languageCode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_key, languageCode);
  }

  static Future<Locale?> getSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final code = prefs.getString(_key);
    return code != null ? Locale(code) : null;
  }
}

Localization Without Context

Sometimes you need translations outside the widget tree. Here are reliable patterns.

Using a Global Navigator Key

final navigatorKey = GlobalKey<NavigatorState>();

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomeScreen(),
    );
  }
}

// Access translations anywhere
AppLocalizations get l10n =>
    AppLocalizations.of(navigatorKey.currentContext!)!;

Using a Static Extension

extension AppLocalizationsX on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this)!;
}

// Usage in widgets
Text(context.l10n.welcomeMessage('User'))

Testing Localized Apps

Unit Testing Translations

Verify that all locale files have matching keys and no missing translations:

import 'dart:convert';
import 'dart:io';
import 'package:test/test.dart';

void main() {
  test('All ARB files have matching keys', () {
    final l10nDir = Directory('lib/l10n');
    final arbFiles = l10nDir.listSync().whereType<File>().where(
          (f) => f.path.endsWith('.arb'),
        );

    final templateFile = arbFiles.firstWhere(
      (f) => f.path.contains('app_en.arb'),
    );
    final templateKeys = (jsonDecode(templateFile.readAsStringSync())
            as Map<String, dynamic>)
        .keys
        .where((k) => !k.startsWith('@'))
        .toSet();

    for (final file in arbFiles) {
      final keys = (jsonDecode(file.readAsStringSync())
              as Map<String, dynamic>)
          .keys
          .where((k) => !k.startsWith('@'))
          .toSet();

      final missing = templateKeys.difference(keys);
      expect(missing, isEmpty,
          reason: '${file.path} is missing keys: $missing');
    }
  });
}

Widget Testing with Locales

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Widget buildLocalizedWidget(Widget child, {Locale locale = const Locale('en')}) {
  return MaterialApp(
    locale: locale,
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    supportedLocales: AppLocalizations.supportedLocales,
    home: child,
  );
}

void main() {
  testWidgets('Widget shows English text', (tester) async {
    await tester.pumpWidget(buildLocalizedWidget(const HomeScreen()));
    await tester.pumpAndSettle();
    expect(find.text('My App'), findsOneWidget);
  });

  testWidgets('Widget shows Arabic text', (tester) async {
    await tester.pumpWidget(
      buildLocalizedWidget(const HomeScreen(), locale: const Locale('ar')),
    );
    await tester.pumpAndSettle();
    expect(find.text('تطبيقي'), findsOneWidget);
  });
}

Production Deployment Checklist

Before releasing your localized Flutter app:

  1. Verify all translations are complete: Run flutter gen-l10n and check for warnings about missing translations.

  2. Test every supported locale: Run through core user flows in each language to catch layout overflow, text truncation, and formatting issues.

  3. Validate RTL layouts: Test with Arabic or Hebrew to ensure mirroring works, icons swap correctly, and no hardcoded left/right values remain.

  4. Check pluralization rules: Verify plural forms for languages with complex rules (Arabic has six forms, Polish has four).

  5. Test number and date formatting: Ensure currencies, dates, and numbers display correctly for each locale.

  6. Localize app store metadata: Translate your app title, description, screenshots, and keywords for each target market.

  7. Set up fallback locale: Configure a fallback locale for unsupported language codes to avoid crashes.

  8. Measure text overflow: Long translations (German averages 30% longer than English) can break fixed-width layouts.

Common Pitfalls and Solutions

Missing Translations Crash

Problem: AppLocalizations.of(context) returns null.

Solution: Ensure localizationsDelegates and supportedLocales are set on MaterialApp, and the widget is below it in the tree.

Text Overflow in Other Languages

Problem: German or Finnish translations overflow containers.

Solution: Use Expanded, Flexible, or FittedBox instead of fixed widths. Test with the longest expected translation.

Hardcoded Strings

Problem: Some text stays in English after adding translations.

Solution: Search your codebase for string literals in widget code. Every user-visible string should come from AppLocalizations.

Wrong Plural Form

Problem: "1 items" appears instead of "1 item".

Solution: Use ICU plural syntax in ARB files and provide all required plural forms for each language.

RTL Icon Direction

Problem: Forward arrows point left in RTL mode.

Solution: Use Directionality.of(context) to conditionally flip directional icons.

Conclusion

Flutter localization transforms your app from single-language to globally accessible. Start with the setup outlined here, add translations incrementally, and test each locale before release. The combination of ARB files, generated localization classes, and Flutter's built-in RTL support gives you everything needed to build apps that feel native in every language.

Further Reading