Flutter Localization (i18n): Building Multi-Language Apps

12 min readFebruary 9, 2026Updated: Mar 9, 2026
Flutter localizationFlutter i18nFlutter multi languageFlutter intlFlutter ARBFlutter localeFlutter translationFlutter internationalization

# Flutter Localization (i18n): Building Multi-Language Apps

Localization is not a nice-to-have — it is a core product capability for any app targeting a global audience. In apps I've shipped to 10+ markets, getting i18n right from day one saved weeks of refactoring later. This guide walks through a complete Flutter localization setup, from initial configuration to RTL support and team translation workflows.

Setting Up Localization from Scratch

Step 1: Add Dependencies

dart
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^class="code-number">0.19.class="code-number">0

flutter:
  generate: true

The `generate: true` flag tells Flutter to auto-generate localization code from your ARB files.

Step 2: Create the l10n Configuration

Create `l10n.yaml` in your project root:

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

Step 3: Create ARB Files

ARB (Application Resource Bundle) files are JSON-based and serve as your single source of truth. Create `lib/l10n/app_en.arb`:

dart
{
  class="code-string">"@@locale": class="code-string">"en",
  class="code-string">"appTitle": class="code-string">"My Application",
  class="code-string">"@appTitle": { class="code-string">"description": class="code-string">"The title of the application" },
  class="code-string">"welcomeMessage": class="code-string">"Welcome, {userName}!",
  class="code-string">"@welcomeMessage": {
    class="code-string">"placeholders": { class="code-string">"userName": { class="code-string">"type": class="code-string">"String", class="code-string">"example": class="code-string">"John" } }
  },
  class="code-string">"itemCount": class="code-string">"{count, plural, =class="code-number">0{No items} =class="code-number">1{class="code-number">1 item} other{{count} items}}",
  class="code-string">"@itemCount": {
    class="code-string">"placeholders": { class="code-string">"count": { class="code-string">"type": class="code-string">"num", class="code-string">"format": class="code-string">"compact" } }
  },
  class="code-string">"lastLogin": class="code-string">"Last login: {date}",
  class="code-string">"@lastLogin": {
    class="code-string">"placeholders": { class="code-string">"date": { class="code-string">"type": class="code-string">"DateTime", class="code-string">"format": class="code-string">"yMMMd" } }
  }
}

Then create `lib/l10n/app_tr.arb` for Turkish:

dart
{
  class="code-string">"@@locale": class="code-string">"tr",
  class="code-string">"appTitle": class="code-string">"Uygulamam",
  class="code-string">"welcomeMessage": class="code-string">"Hos geldin, {userName}!",
  class="code-string">"itemCount": class="code-string">"{count, plural, =class="code-number">0{Oge yok} =class="code-number">1{class="code-number">1 oge} other{{count} oge}}",
  class="code-string">"lastLogin": class="code-string">"Son giris: {date}"
}

Step 4: Configure MaterialApp

dart
import class="code-string">'package:flutter_localizations/flutter_localizations.dart';
import class="code-string">'package:flutter_gen/gen_l10n/app_localizations.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [Locale(class="code-string">'en'), Locale(class="code-string">'tr'), Locale(class="code-string">'de')],
      locale: const Locale(class="code-string">'en'),
      home: const HomeScreen(),
    );
  }
}

Step 5: Access Translations in Widgets

dart
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: Column(children: [
        Text(l10n.welcomeMessage(class="code-string">'John')),
        Text(l10n.itemCount(class="code-number">5)),
        Text(l10n.lastLogin(DateTime.now())),
      ]),
    );
  }
}

Dynamic Locale Switching

Users should be able to switch languages without restarting the app. A clean pattern uses a `ChangeNotifier`:

dart
class LocaleProvider extends ChangeNotifier {
  Locale _locale = const Locale(class="code-string">'en');
  Locale get locale => _locale;
  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }
}

class=class="code-string">"code-comment">// In your MaterialApp:
Consumer<LocaleProvider>(
  builder: (context, provider, _) => MaterialApp(
    locale: provider.locale,
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    supportedLocales: AppLocalizations.supportedLocales,
    home: const HomeScreen(),
  ),
)

Pluralization and Gender

The ICU message format handles pluralization elegantly. Flutter supports `plural`, `select`, and `gender` forms directly in ARB files:

dart
{
  class="code-string">"notificationCount": class="code-string">"{count, plural, =class="code-number">0{No notifications} =class="code-number">1{class="code-number">1 notification} other{{count} notifications}}",
  class="code-string">"@notificationCount": { class="code-string">"placeholders": { class="code-string">"count": { class="code-string">"type": class="code-string">"num" } } },
  class="code-string">"genderGreeting": class="code-string">"{gender, select, male{Mr. {name}} female{Ms. {name}} other{Dear {name}}}",
  class="code-string">"@genderGreeting": {
    class="code-string">"placeholders": { class="code-string">"gender": { class="code-string">"type": class="code-string">"String" }, class="code-string">"name": { class="code-string">"type": class="code-string">"String" } }
  }
}

Languages like Polish, Arabic, and Russian have complex plural rules beyond singular/plural. The ICU format handles `zero`, `one`, `two`, `few`, `many`, and `other` categories automatically based on locale.

Date and Number Formatting

Never format dates or numbers manually. Always use `intl`:

dart
import class="code-string">'package:intl/intl.dart';

final dateFormatter = DateFormat.yMMMd(class="code-string">'tr');
print(dateFormatter.format(DateTime.now())); class=class="code-string">"code-comment">// "class="code-number">9 Mar class="code-number">2026"

final currencyFormatter = NumberFormat.currency(locale: class="code-string">'de', symbol: class="code-string">'\u20ac');
print(currencyFormatter.format(class="code-number">1234.56)); class=class="code-string">"code-comment">// "class="code-number">1.234,class="code-number">56 \u20ac"

final compactFormatter = NumberFormat.compact(locale: class="code-string">'en');
print(compactFormatter.format(class="code-number">1500000)); class=class="code-string">"code-comment">// "class="code-number">1.5M"

RTL Language Support

Supporting right-to-left languages like Arabic, Hebrew, and Farsi requires careful layout attention.

Flutter handles text direction automatically via `flutter_localizations`. The key rule: replace hardcoded `left`/`right` with `start`/`end`:

dart
class=class="code-string">"code-comment">// Bad - breaks in RTL
Padding(padding: EdgeInsets.only(left: class="code-number">16.0))

class=class="code-string">"code-comment">// Good - respects text direction
Padding(padding: EdgeInsetsDirectional.only(start: class="code-number">16.0))

Some icons need mirroring in RTL layouts:

dart
Widget buildArrowIcon(BuildContext context) {
  final isRtl = Directionality.of(context) == TextDirection.rtl;
  return Transform.flip(flipX: isRtl, child: const Icon(Icons.arrow_forward));
}

In apps I've shipped to 10+ markets, I always test with Arabic even if the app does not officially support it, just to catch directional layout bugs early.

Translation Workflow for Teams

Organizing ARB Files

I recommend domain-based key naming to keep things manageable as projects grow:

dart
{
  class="code-string">"auth_loginButton": class="code-string">"Log In",
  class="code-string">"auth_signupButton": class="code-string">"Sign Up",
  class="code-string">"profile_editTitle": class="code-string">"Edit Profile",
  class="code-string">"settings_languageLabel": class="code-string">"Language",
  class="code-string">"error_networkTimeout": class="code-string">"Connection timed out. Please try again."
}

Integrating with Translation Services

For production apps, I use one of these workflows:

  • **Manual translation**: Export ARBs, send to translators, import back. Works for small projects with fewer than 100 strings.
  • **Localizely / Crowdin / Phrase**: Upload your template ARB, translators work in a web UI, download completed ARBs. These tools handle plural rules and flag missing translations.
  • **CI/CD integration**: A GitHub Action that checks for missing keys on every PR, preventing incomplete translations from shipping.
  • Missing Translation Detection

    A simple Dart script can parse all ARB files, compare keys against the template, and fail CI if any translations are missing. I keep a `scripts/check_translations.dart` in every project that does exactly this — it reads `app_en.arb` as the source of truth and diffs every other ARB file against it.

    Common i18n Mistakes

    Mistake 1: Hardcoded Strings

    Every user-facing string must go through the localization system — including error messages, tooltips, and accessibility labels.

    dart
    class=class="code-string">"code-comment">// Wrong
    Text(class="code-string">'Welcome back!')
    class=class="code-string">"code-comment">// Right
    Text(AppLocalizations.of(context).welcomeBack)

    Mistake 2: String Concatenation

    Never build sentences by concatenating translated fragments. Word order varies between languages.

    dart
    class=class="code-string">"code-comment">// Wrong - word order breaks in other languages
    Text(l10n.you + class="code-string">' ' + l10n.have + class="code-string">' ' + count.toString() + class="code-string">' ' + l10n.messages)
    class=class="code-string">"code-comment">// Right - use a single parameterized string
    Text(l10n.messageCount(count))

    Mistake 3: No Fallback Locale

    If a translation is missing and there is no fallback, your app may crash or show raw keys. Flutter automatically falls back to the first locale in `supportedLocales`, so always put your primary language first.

    Mistake 4: Ignoring Text Expansion

    German text is typically 30% longer than English. Arabic can be even wider. Always test with long translations. I use pseudo-localization during development — wrapping strings with extra characters to simulate expansion.

    Mistake 5: Hardcoded Date and Number Formats

    dart
    class=class="code-string">"code-comment">// Wrong - US-only format
    Text(class="code-string">'${date.month}/${date.day}/${date.year}')
    class=class="code-string">"code-comment">// Right - locale-aware
    Text(DateFormat.yMd(Localizations.localeOf(context).toString()).format(date))

    Mistake 6: Forgetting Accessibility

    Screen readers rely on localized `Semantics` labels. If you only localize visible text, visually impaired users in other locales get a broken experience:

    dart
    Semantics(
      label: AppLocalizations.of(context).closeButtonLabel,
      child: IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
    )

    Testing Localization

    dart
    testWidgets(class="code-string">'displays Turkish welcome message', (tester) async {
      await tester.pumpWidget(MaterialApp(
        locale: const Locale(class="code-string">'tr'),
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        home: const HomeScreen(),
      ));
      expect(find.text(class="code-string">'Hos geldin, John!'), findsOneWidget);
    });

    Conclusion

    A solid i18n setup reduces release friction significantly when entering new markets. The investment you make early in proper ARB structure, locale-aware formatting, and a clean translation workflow pays for itself many times over. In apps I've shipped to 10+ markets, the teams that treated localization as a first-class architectural concern scaled smoothly to new regions without emergency hotfixes.

    I can help you implement a scalable localization pipeline — from initial ARB setup to CI-integrated translation workflows.

    Related Articles

    Have a Flutter Project?

    I build high-performance Flutter applications for iOS, Android, and web.

    Get in Touch