← Articles

Internationalization (i18n) in Flutter

By Ann Tech · 26 November 2025

Internationalization (i18n) means making your app work in multiple languages and locales. Flutter's built-in localization system, combined with the intl package, handles translations, plural forms, date/number formatting, and text directionality.

Setup

dependencies:
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

flutter:
  generate: true  # Enable code generation for ARB files

Create l10n.yaml at the project root:

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

ARB files

ARB (Application Resource Bundle) is Flutter's translation format:

// lib/l10n/app_en.arb
{
  "@@locale": "en",
  "appTitle": "My App",
  "@appTitle": {
    "description": "The application title shown in the app bar"
  },
  "welcomeMessage": "Welcome, {name}!",
  "@welcomeMessage": {
    "description": "Welcome message on the home screen",
    "placeholders": {
      "name": {
        "type": "String",
        "example": "Alice"
      }
    }
  },
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "description": "Number of items in the cart",
    "placeholders": {
      "count": {
        "type": "num",
        "format": "compact"
      }
    }
  },
  "orderDate": "Ordered on {date}",
  "@orderDate": {
    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "yMMMMd"
      }
    }
  }
}
// lib/l10n/app_fr.arb
{
  "@@locale": "fr",
  "appTitle": "Mon Application",
  "welcomeMessage": "Bienvenue, {name} !",
  "itemCount": "{count, plural, =0{Aucun article} =1{1 article} other{{count} articles}}",
  "orderDate": "Commandé le {date}"
}

Run the generator:

flutter gen-l10n

Configuring MaterialApp

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

MaterialApp(
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: AppLocalizations.supportedLocales,
  // Optional: override system locale
  locale: userSelectedLocale,
)

Using translations in widgets

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Column(
        children: [
          Text(l10n.welcomeMessage(user.name)),
          Text(l10n.itemCount(cart.itemCount)),
          Text(l10n.orderDate(order.date)),
        ],
      ),
    );
  }
}

Locale switching at runtime

Let users change language in settings:

final localeProvider = StateProvider<Locale?>((ref) => null);

// In MaterialApp
locale: ref.watch(localeProvider),

// Settings screen
class LanguageSelector extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return DropdownButton<Locale>(
      value: ref.watch(localeProvider),
      items: [
        const DropdownMenuItem(value: Locale('en'), child: Text('English')),
        const DropdownMenuItem(value: Locale('fr'), child: Text('Français')),
        const DropdownMenuItem(value: Locale('de'), child: Text('Deutsch')),
        const DropdownMenuItem(value: Locale('ar'), child: Text('عربي')),
      ],
      onChanged: (locale) {
        ref.read(localeProvider.notifier).state = locale;
        // Persist to shared_preferences
      },
    );
  }
}

Number and date formatting with intl

The generated code handles basic formatting, but for custom formats use intl directly:

import 'package:intl/intl.dart';

// Currency
final price = NumberFormat.currency(
  locale: Localizations.localeOf(context).toString(),
  symbol: '\$',
).format(order.totalAmount);

// Compact numbers
final followers = NumberFormat.compact().format(1200000); // "1.2M"

// Relative time
final daysAgo = DateFormat.yMMMd().format(date);

RTL support

Arabic, Hebrew, and Persian text is right-to-left. Flutter handles this automatically when GlobalWidgetsLocalizations.delegate is included and the locale is set correctly.

For custom widgets, use Directionality to detect and adapt:

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

Row(
  textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
  children: [
    const Icon(Icons.arrow_back),
    const SizedBox(width: 8),
    Text(l10n.backButton),
  ],
)

Or use EdgeInsetsDirectional instead of EdgeInsets for padding that flips automatically:

// This flips in RTL without extra code
Padding(
  padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
)

Testing localizations

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

testWidgets('shows French translation', (tester) async {
  await tester.pumpWidget(
    buildLocalized(const HomeScreen(), locale: const Locale('fr')),
  );
  expect(find.text('Mon Application'), findsOneWidget);
});

Common pitfalls

Missing translations. If app_fr.arb is missing a key that app_en.arb has, the generator either errors or falls back to the template language. Set untranslated-messages-file in l10n.yaml to catch gaps.

Pluralization without ICU format. Simple string replacement doesn't handle plurals correctly ("1 items" instead of "1 item"). Always use ICU plural syntax in ARB files.

Hardcoded locale in tests. Tests that run in a fixed locale may pass in English but fail when the locale changes because a date or number is formatted differently. Use buildLocalized helpers with an explicit locale.

Sign in to like, dislike, or report.

Internationalization (i18n) in Flutter — ANN Tech