Flutter

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

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: ^0.19.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
{
  "@@locale": "en",
  "appTitle": "My Application",
  "@appTitle": { "description": "The title of the application" },
  "welcomeMessage": "Welcome, {userName}!",
  "@welcomeMessage": {
    "placeholders": { "userName": { "type": "String", "example": "John" } }
  },
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "placeholders": { "count": { "type": "num", "format": "compact" } }
  },
  "lastLogin": "Last login: {date}",
  "@lastLogin": {
    "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" } }
  }
}

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

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

Step 4: Configure MaterialApp

dart
import 'package:flutter_localizations/flutter_localizations.dart';
import '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('en'), Locale('tr'), Locale('de')],
      locale: const Locale('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('John')),
        Text(l10n.itemCount(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('en');
  Locale get locale => _locale;
  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }
}

// 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
{
  "notificationCount": "{count, plural, =0{No notifications} =1{1 notification} other{{count} notifications}}",
  "@notificationCount": { "placeholders": { "count": { "type": "num" } } },
  "genderGreeting": "{gender, select, male{Mr. {name}} female{Ms. {name}} other{Dear {name}}}",
  "@genderGreeting": {
    "placeholders": { "gender": { "type": "String" }, "name": { "type": "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 'package:intl/intl.dart';

final dateFormatter = DateFormat.yMMMd('tr');
print(dateFormatter.format(DateTime.now())); // "9 Mar 2026"

final currencyFormatter = NumberFormat.currency(locale: 'de', symbol: '\u20ac');
print(currencyFormatter.format(1234.56)); // "1.234,56 \u20ac"

final compactFormatter = NumberFormat.compact(locale: 'en');
print(compactFormatter.format(1500000)); // "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
// Bad - breaks in RTL
Padding(padding: EdgeInsets.only(left: 16.0))

// Good - respects text direction
Padding(padding: EdgeInsetsDirectional.only(start: 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
{
  "auth_loginButton": "Log In",
  "auth_signupButton": "Sign Up",
  "profile_editTitle": "Edit Profile",
  "settings_languageLabel": "Language",
  "error_networkTimeout": "Connection timed out. Please try again."
}

Integrating with Translation Services

For production apps, I use one of these workflows:

  1. Manual translation: Export ARBs, send to translators, import back. Works for small projects with fewer than 100 strings.
  2. 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.
  3. 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
// Wrong
Text('Welcome back!')
// Right
Text(AppLocalizations.of(context).welcomeBack)

Mistake 2: String Concatenation

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

dart
// Wrong - word order breaks in other languages
Text(l10n.you + ' ' + l10n.have + ' ' + count.toString() + ' ' + l10n.messages)
// 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
// Wrong - US-only format
Text('${date.month}/${date.day}/${date.year}')
// 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('displays Turkish welcome message', (tester) async {
  await tester.pumpWidget(MaterialApp(
    locale: const Locale('tr'),
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    supportedLocales: AppLocalizations.supportedLocales,
    home: const HomeScreen(),
  ));
  expect(find.text('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