Skip to main content
Back to all tutorials
Tested: Flutter 3.24.3 • Dart 3.5.3
T08.06 Verified September 2026
Localization i18n l10n ARB ICU MessageFormat RTL Directionality gen-l10n

Localize Text, RTL Layouts, Placeholders and Plural Forms

Deconstruct how to configure Flutter localization with ARB bundles, enforce mandatory ICU fallback branches, handle literal quote escaping, and mirror UI layouts with Directionality.

1. The Core Challenge & Problem

Building internationalized Flutter applications involves far more than simply translating English strings to Spanish or Arabic. In production, naive internationalization setups encounter four critical failures: 1. **Compilation Crashes from Missing Fallback Branches**: In ICU MessageFormat, plural expressions (`{count, plural, ...}`) and select expressions (`{gender, select, ...}`) **strictly require** an `other` fallback branch. Omitting `other` causes Flutter's code generator (`flutter gen-l10n`) to abort with fatal syntax errors during app compilation. 2. **Broken Literal Braces & Apostrophe Corruption**: When a string requires literal braces (e.g. `To define a set, write '{'value'}' in Dart.`) or apostrophes (e.g. `Don''t touch`), failing to enable and configure `--use-escaping` in `l10n.yaml` corrupts the generated Dart strings or triggers lexer errors. 3. **Rigid Placeholder Ordering**: Grammatical structure differs across languages. A string like `"{firstName} {lastName}"` in English must often be reordered to `"{lastName}, {firstName}"` in Japanese, Hungarian, or Spanish official documents. Naive string regex validators falsely flag reordered placeholders as errors. 4. **Hardcoded Left-to-Right Layout Assumptions**: Using physical alignments (`Alignment.topLeft`, `EdgeInsets.only(left: 16)`) prevents screens from mirroring properly in Right-to-Left (RTL) locales such as Arabic, Hebrew, Persian, or Urdu.

2. Architectural Principles & Resolution

Robust Flutter internationalization requires combining standard ARB resources, strict ICU syntax validation, and directional layout primitives: ### 1. The Flutter ARB & ICU Subset Contract Flutter's official `flutter_localizations` package parses Application Resource Bundle (`.arb`) files conforming to RFC 8259 JSON: - **Base Template Isolation**: `app_en.arb` declares all message keys and entry metadata (`@key` containing `description` and typed `placeholders`). - **Translation Bundles**: Target files (`app_es.arb`, `app_ar.arb`) supply localized strings. Target bundles are not required to duplicate `@metadata`. - **Mandatory Fallback Branches**: Every `plural` and `select` statement **must include an `other` branch**. - **Quote & Brace Escaping**: When `use-escaping: true` is configured in `l10n.yaml`, consecutive single quotes (`''`) escape literal apostrophes, and `'{'` and `'}'` escape literal braces. - **Permitted Placeholder Reordering**: Translations may reorder simple placeholders to match grammatical syntax without failing parity checks. ### 2. Recommended Project Configuration (`l10n.yaml`) Place this configuration file at your project root: ```yaml arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart output-class: AppLocalizations use-escaping: true nullable-getter: false ``` And in your `pubspec.yaml`: ```yaml dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: any flutter: generate: true ``` ### 3. Directional Layout Primitives for RTL Support Never use physical directional constants in translatable layouts. Use their directional equivalents: - Replace `EdgeInsets.only(left: 16)` with `EdgeInsetsDirectional.only(start: 16)`. - Replace `Alignment.topLeft` with `AlignmentDirectional.topStart`. - Replace `Positioned(left: 0)` with `PositionedDirectional(start: 0)`. - Wrap preview trees in `Directionality(textDirection: ...)` during testing to verify bidirectional mirroring.

3. Complete Tested Flutter Code

main.dart (Flutter 3.x+ ready)
lib/main.dart Copy & Paste into a new Flutter project
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

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

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

  @override
  State<LocalizedApp> createState() => _LocalizedAppState();
}

class _LocalizedAppState extends State<LocalizedApp> {
  Locale _locale = const Locale('en');
  int _itemCount = 1;

  void _toggleLocale() {
    setState(() {
      _locale = _locale.languageCode == 'en'
          ? const Locale('ar') // Demonstrates RTL layout mirroring
          : const Locale('en');
    });
  }

  @override
  Widget build(BuildContext context) {
    final isRtl = _locale.languageCode == 'ar';

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      locale: _locale,
      supportedLocales: const [Locale('en'), Locale('es'), Locale('ar')],
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: const Color(0xFF8467D7),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text(isRtl ? 'معاينة التعريب' : 'Localization Preview'),
          actions: [
            IconButton(
              icon: const Icon(Icons.language),
              tooltip: 'Switch Language',
              onPressed: _toggleLocale,
            ),
          ],
        ),
        body: Padding(
          padding: const EdgeInsetsDirectional.all(16.0), // Mirrors in RTL
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // User Card with Directional Avatar Alignment
              Card(
                child: Padding(
                  padding: const EdgeInsetsDirectional.all(16.0),
                  child: Row(
                    children: [
                      const CircleAvatar(child: Icon(Icons.person)),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              isRtl ? 'الملف الشخصي' : 'User Profile',
                              style: const TextStyle(fontSize: 12, color: Colors.grey),
                            ),
                            Text(
                              // In English: "Chirag Raval (Lead)"
                              // In RTL/Arabic: "راوال، شيراغ (المسؤول)"
                              isRtl ? 'راوال، شيراغ (المسؤول)' : 'Chirag Raval (Lead Engineer)',
                              style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),

              // Interactive Plural Counter
              Card(
                child: Padding(
                  padding: const EdgeInsetsDirectional.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        isRtl
                            ? (_itemCount == 0 ? 'لا توجد عناصر' : '$_itemCount عناصر')
                            : (_itemCount == 0
                                ? 'No items'
                                : _itemCount == 1
                                    ? '1 item'
                                    : '$_itemCount items'),
                        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 12),
                      Row(
                        children: [
                          OutlinedButton.icon(
                            onPressed: () {
                              if (_itemCount > 0) setState(() => _itemCount--);
                            },
                            icon: const Icon(Icons.remove, size: 14),
                            label: Text(isRtl ? 'إنقاص' : 'Decrement'),
                          ),
                          const SizedBox(width: 8),
                          ElevatedButton.icon(
                            onPressed: () => setState(() => _itemCount++),
                            icon: const Icon(Icons.add, size: 14),
                            label: Text(isRtl ? 'زيادة' : 'Increment'),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

4. Expected Visual & Behavioral Result

A fully responsive, directional Material 3 Flutter application that cleanly mirrors layouts between English LTR and Arabic RTL, demonstrates plural branching, and complies with Flutter gen-l10n standards.

5. Common Pitfalls & Traps

Pitfall #1: Omitting the mandatory "other" fallback branch in plural or select ARB expressions.

Remedy: Always supply an "other" branch in every plural and select message (e.g. "{count, plural, =0{none} =1{one} other{{count} items}}"). Flutter's gen-l10n strictly requires "other" and aborts compilation if it is missing.

Pitfall #2: Using physical EdgeInsets.only(left: ...) and Alignment.topLeft in translatable screens.

Remedy: Use EdgeInsetsDirectional.only(start: ...) and AlignmentDirectional.topStart so layouts automatically mirror in RTL languages like Arabic and Hebrew.

Pitfall #3: Forgetting --use-escaping in l10n.yaml when using apostrophes or literal braces.

Remedy: Set "use-escaping: true" in l10n.yaml so that pairs of single quotes ("''") produce literal single quotes and "'{'" produces literal braces.

Pitfall #4: Treating placeholder reordering in translation bundles as syntax errors.

Remedy: Translations often require different grammatical word orders. Reordered placeholders (e.g. "{lastName}, {firstName}") are completely valid as long as all required variables are preserved.

Official Flutter References & Standards