Dart Tutorial

Flutter Lesson 72 of 83 4 min read

Localization in Flutter: Supporting Multiple Languages

Translate a Flutter app with flutter_localizations and ARB files: setup, placeholders, plurals, dates, right-to-left and language switching.

On this page

Internationalization (i18n) is preparing an app so it can be translated. Localization (l10n) is the translation itself, plus formatting dates, numbers and currency the way each region expects. Flutter has an official system for both.

Setup #

1. Add the packages.

flutter pub add flutter_localizations --sdk=flutter
flutter pub add intl:any

2. Turn on code generation in pubspec.yaml:

flutter:
  uses-material-design: true
  generate: true

3. Create l10n.yaml in the project root:

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

4. Write the template file, lib/l10n/app_en.arb:

{
  "@@locale": "en",
  "appTitle": "My Shop",
  "welcome": "Welcome back",
  "greeting": "Hello, {name}!",
  "@greeting": {
    "description": "Greeting on the home screen",
    "placeholders": {
      "name": { "type": "String", "example": "Asha" }
    }
  },
  "cartItems": "{count, plural, =0{Your cart is empty} =1{1 item in your cart} other{{count} items in your cart}}",
  "@cartItems": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}

5. Add a translation, lib/l10n/app_ne.arb:

{
  "@@locale": "ne",
  "appTitle": "मेरो पसल",
  "welcome": "फेरि स्वागत छ",
  "greeting": "नमस्ते, {name}!",
  "cartItems": "{count, plural, =0{तपाईंको कार्ट खाली छ} =1{कार्टमा १ वस्तु} other{कार्टमा {count} वस्तु}}"
}

6. Generate the Dart code. It happens automatically on flutter run and flutter pub get, or run:

flutter gen-l10n

Wiring it into the app #

import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomePage(),
    );
  }
}

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Column(
        children: [
          Text(l10n.welcome),
          Text(l10n.greeting('Asha')),
          Text(l10n.cartItems(3)),
        ],
      ),
    );
  }
}

The import path depends on your Flutter version and the output-dir setting. Older versions generated the file as package:flutter_gen/gen_l10n/app_localizations.dart. If the import above is not found, check where flutter gen-l10n wrote the file.

Every key in the ARB file becomes a typed getter or method. A misspelt key is a compile error, and so is a missing argument.

localizationsDelegates also translates Flutter’s own widgets: date pickers, the back button tooltip, “Cancel” and “OK”.

A shortcut #

AppLocalizations.of(context)! on every line gets tiresome.

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

// Usage
Text(context.l10n.welcome)

Plurals and selects #

Languages have different plural rules. English has two forms, Arabic has six, Japanese has one. Never build plurals with if (count == 1). Use the ICU plural syntax shown above and let each translation define its own forms.

select chooses by a keyword, such as gender or status:

"orderStatus": "{status, select, pending{Waiting for payment} shipped{On its way} delivered{Delivered} other{Unknown}}",
"@orderStatus": { "placeholders": { "status": { "type": "String" } } }

Dates, numbers and currency #

Different regions write these differently. Format them with intl, passing the current locale.

import 'package:intl/intl.dart';

final locale = Localizations.localeOf(context).toString();

DateFormat.yMMMMd(locale).format(DateTime.now());           // October 10, 2026
NumberFormat.decimalPattern(locale).format(1234567.89);     // 1,234,567.89
NumberFormat.currency(locale: locale, symbol: 'Rs. ').format(1499.5);
NumberFormat.compact(locale: locale).format(12500);         // 12.5K

ARB placeholders can format for you:

"lastSeen": "Last seen {date}",
"@lastSeen": {
  "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" } }
}

Letting the user choose the language #

By default the app follows the device language. To offer a choice, set locale on MaterialApp and store the selection.

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Locale? _locale; // null means: follow the device

  void _setLocale(Locale? locale) => setState(() => _locale = locale);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      locale: _locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: SettingsPage(onLocaleChanged: _setLocale),
    );
  }
}

Save the language code with shared_preferences and restore it at start-up.

Right-to-left languages #

Arabic, Hebrew, Persian and Urdu are read from right to left. Flutter mirrors Row, ListTile, app bars and navigation automatically. To keep your own layouts correct, use start and end in place of left and right.

UseNot
EdgeInsetsDirectional.only(start: 16)EdgeInsets.only(left: 16)
AlignmentDirectional.centerStartAlignment.centerLeft
PositionedDirectional(start: 0)Positioned(left: 0)
TextAlign.startTextAlign.left

Test by forcing a direction:

Directionality(textDirection: TextDirection.rtl, child: const MyScreen())

iOS #

Add each supported language to the Localizations list in Xcode (Runner, Info), or to CFBundleLocalizations in Info.plist. Otherwise the App Store lists only English, and iOS may not offer your translations.

Writing translatable text #

  • Never join strings. 'Hello ' + name + ', you have ' + ... cannot be translated, because word order differs between languages. Use one message with placeholders.
  • Leave room. German and Finnish text is often 30 percent longer than English. Let text wrap, and avoid fixed widths.
  • Add descriptions with @key entries. Translators see only the string, with no screen for context.
  • No text in images.
  • Translate everything the user sees: error messages, notifications, the store listing.

Missing translations #

List what has not been translated yet:

# l10n.yaml
untranslated-messages-file: untranslated.json

Messages missing from a language fall back to the template language.

Try it yourself #

Add two languages to an app you have built. Move every visible string into ARB files, including one message with a placeholder and one with a plural. Add a language picker in settings that takes effect immediately and is remembered after a restart.

Practise in the playground Updated by Santosh Adhikari