The Complete Guide to Flutter Localization

From ARB files to RTL support: localize your Flutter app with easy_localization, handle plurals for every language, and automate translations with AI.

1

Install easy_localization

Add the easy_localization package to your pubspec.yaml. This is the most popular Flutter i18n package with support for ARB/JSON files, plurals, and context extensions. Also add flutter_localizations from the SDK for locale-aware formatting of dates, numbers, and text direction.

easy_localization provides context extensions like context.tr() and 'key'.tr() for concise translation access. It handles locale loading, persistence, and hot-reloading of translation files during development.
pubspec.yaml
dependencies:
  easy_localization: ^3.0.7
  flutter_localizations:
    sdk: flutter
2

Create ARB Translation Files

Application Resource Bundle (ARB) files are the standard localization format for Flutter. Each file contains key-value pairs with optional metadata describing placeholders, plural rules, and context for translators. Create one file per locale in your assets/translations directory.

assets/translations/en.arb
{
  "@@locale": "en",
  "appTitle": "My App",
  "@appTitle": {
    "description": "The title of the application"
  },
  "greeting": "Hello, {name}!",
  "@greeting": {
    "description": "Greeting with user name",
    "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"
      }
    }
  }
}
The @-prefixed keys (like @greeting) are metadata — they describe the string above them. Include placeholder types and examples to help translators produce accurate translations. These metadata keys are stripped at runtime and add zero overhead.
3

Configure the App

Wrap your app with the EasyLocalization widget. It manages locale state, loads translations from your asset files, and provides the locale delegates that MaterialApp needs. The three required delegate properties are localizationsDelegates, supportedLocales, and locale — all available via context extensions.

lib/main.dart
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();

  runApp(
    EasyLocalization(
      supportedLocales: [
        Locale('en'),
        Locale('ar'),
        Locale('ja'),
        Locale('de'),
      ],
      path: 'assets/translations',  // Path to your ARB/JSON files
      fallbackLocale: Locale('en'),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: context.localizationDelegates,
      supportedLocales: context.supportedLocales,
      locale: context.locale,
      home: HomePage(),
    );
  }
}
"Easy Localization not initialized" — this error means you forgot to call EasyLocalization.ensureInitialized() before runApp(). It must be awaited after WidgetsFlutterBinding.ensureInitialized() and before runApp().
4

Translate Widgets

Use the .tr() extension method on string keys to get translated text in any widget. For plurals, use .plural() with the count value. easy_localization provides both the string extension syntax ('key'.tr()) and the context method syntax (context.tr('key')).

lib/widgets/home_page.dart
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('appTitle'.tr()),   // Simple key
      ),
      body: Column(
        children: [
          // String with variable interpolation
          Text('greeting'.tr(args: ['Alice'])),

          // Named arguments
          Text('greeting'.tr(namedArgs: {'name': 'Alice'})),

          // Plural form
          Text('itemCount'.plural(3)),
        ],
      ),
    );
  }
}
If translations return raw keys instead of translated text, check: 1) The EasyLocalization widget wraps your MaterialApp, not the other way around. 2) Your translation files are declared in pubspec.yaml under assets. 3) The file path in EasyLocalization matches your actual directory structure.
5

Handle Plurals and Variables

Flutter uses ICU MessageFormat for plurals — the same standard used by iOS, Android, and the web. Define plural forms using {count, plural, ...} syntax in your ARB files. Each language needs its own set of forms based on CLDR plural rules. Arabic has 6 forms, Russian has 4, Japanese has 1.

Plural forms by language
// English: 3 useful forms (=0, =1, other)
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"

// Arabic: 6 forms (=0, =1, =2, few, many, other)
"itemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}"

// Japanese: 1 form (other)
"itemCount": "{count, plural, other{{count}個のアイテム}}"

// German: 2 forms (=1, other)
"itemCount": "{count, plural, =1{1 Artikel} other{{count} Artikel}}"

// Russian: 3 forms (one, few, many)
"itemCount": "{count, plural, =0{Нет товаров} one{{count} товар} few{{count} товара} many{{count} товаров} other{{count} товаров}}"
Variables and named arguments
// Simple variable
"welcome": "Welcome, {name}!"

// Multiple variables
"orderStatus": "Order #{orderId} — {status}"

// Variable in plural context
"unreadMessages": "{count, plural, =0{No unread messages} =1{1 unread message from {sender}} other{{count} unread messages}}"

// In widgets:
Text('welcome'.tr(namedArgs: {'name': userName}));
Text('orderStatus'.tr(namedArgs: {'orderId': '1234', 'status': 'Shipped'}));
Text('unreadMessages'.plural(count, namedArgs: {'sender': senderName}));
Never hardcode singular/plural logic with if (count == 1). Languages like French treat 0 as singular. Russian, Polish, and Arabic have plural forms that English lacks entirely. Always use ICU plural syntax and let the framework select the correct form.
6

Support RTL Languages

Flutter automatically mirrors the entire layout when the locale is a right-to-left language (Arabic, Hebrew, Persian, Urdu). But your code must use directional-aware widgets and properties for the mirroring to work correctly. Replace hardcoded left/right with start/end equivalents.

lib/widgets/adaptive_layout.dart
import 'package:flutter/material.dart';

class AdaptiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return Scaffold(
      body: Row(
        children: [
          // Use start/end instead of left/right
          Expanded(
            child: Padding(
              padding: EdgeInsetsDirectional.only(
                start: 16.0,  // Leading edge (left in LTR, right in RTL)
                end: 8.0,     // Trailing edge
              ),
              child: Text('content'.tr()),
            ),
          ),
          // Flip icons for RTL
          Icon(
            isRtl ? Icons.arrow_back : Icons.arrow_forward,
          ),
        ],
      ),
    );
  }
}
RTL-aware widget patterns
// Use Directional widgets for RTL-aware layouts
EdgeInsetsDirectional.only(start: 16, end: 8)   // Not EdgeInsets.only(left: 16, right: 8)
AlignmentDirectional.centerStart                  // Not Alignment.centerLeft
BorderRadiusDirectional.only(topStart: Radius.circular(8))

// Automatically mirrors when locale changes to Arabic, Hebrew, etc.
// No conditional logic needed — Flutter handles text direction from locale
Test RTL by temporarily setting your app locale to Arabic (Locale('ar')). Flutter mirrors the entire UI — navigation drawers open from the right, back arrows flip, and text aligns right. Use EdgeInsetsDirectional, AlignmentDirectional, and BorderRadiusDirectional to ensure your custom layouts mirror correctly.
7

Smart Locale Fallback Chains

By default, when a pt-BR translation is missing, Flutter falls back directly to English — skipping perfectly good pt-PT translations. The locale_chain package fixes this with configurable fallback chains. One line of setup, zero migration — your existing .tr() calls just work.

lib/main.dart with LocaleChain
import 'package:locale_chain/locale_chain.dart';
import 'package:locale_chain/easy_localization.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();
  LocaleChain.configure();  // Enable smart fallback chains

  runApp(
    EasyLocalization(
      supportedLocales: [
        Locale('en'),
        Locale('pt', 'BR'),
        Locale('pt', 'PT'),
        Locale('es'),
        Locale('es', 'MX'),
      ],
      path: 'assets/translations',
      assetLoader: LocaleChainAssetLoader(  // Swap in the chain loader
        baseLoader: RootBundleAssetLoader(),
      ),
      fallbackLocale: Locale('en'),
      child: MyApp(),
    ),
  );
}

// Result: pt-BR user sees pt-PT translations when pt-BR keys are missing,
// instead of falling back directly to English.
Custom fallback configuration
// Override specific chains while keeping built-in defaults
LocaleChain.configure(
  fallbacks: {
    'pt-BR': ['pt-PT', 'pt'],      // pt-BR → pt-PT → pt → en
    'es-MX': ['es-419', 'es'],     // es-MX → es-419 → es → en
    'fr-CA': ['fr'],               // fr-CA → fr → en
  },
);

// Or replace all defaults with your own chains
LocaleChain.configure(
  fallbacks: {
    'zh-Hant-HK': ['zh-Hant-TW', 'zh-Hans'],
    'pt-BR': ['pt-PT', 'pt'],
  },
  mergeDefaults: false,  // Only your chains, no built-in defaults
);
locale_chain includes built-in fallback chains for Portuguese, Spanish, French, German, Italian, Dutch, Norwegian, and Malay regional variants. A pt-BR user will see pt-PT content before English. An es-MX user will see es-419 then es before the default locale.
8

Automate Translations

With your localization setup complete, translate your ARB files using AI. In your IDE, ask your AI assistant to translate your source ARB file, or use the i18n Agent CLI in your CI/CD pipeline. ARB metadata (placeholders, descriptions) provides context that improves translation quality.

Terminal
# In your IDE, ask your AI assistant:
> Translate assets/translations/en.arb to Arabic, Japanese, and German

✓ ar.arb created (1.4s)
✓ ja.arb created (1.2s)
✓ de.arb created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate assets/translations/en.arb --lang ar,ja,de
Translate incrementally — when you add new keys to your source ARB file, translate just the diff rather than regenerating all files. This preserves any human-reviewed translations and keeps your ARB metadata intact.

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with pseudo-translations using i18n-pseudo before real translations arrive.

Common Pitfalls

Translation Files Not Found at Runtime

Your ARB/JSON files must be declared in pubspec.yaml under the assets section. Missing this step means Flutter cannot find the files at runtime even though they exist on disk. Add: assets: - assets/translations/

Invalid ARB File Syntax

ARB files are strict JSON — no trailing commas, no single quotes, no comments. A single syntax error silently prevents the entire file from loading. Validate your ARB files with a JSON linter before debugging translation issues.

Translation Changes Not Appearing on Hot Reload

easy_localization caches translations in memory. Adding new keys or changing existing translations may require a full hot restart (not hot reload) to take effect. During development, use hot restart (Shift+R) after editing translation files.

Hardcoded Left/Right Breaking RTL

Using EdgeInsets.only(left: 16) instead of EdgeInsetsDirectional.only(start: 16) prevents Flutter from mirroring your layout for RTL languages. Search your codebase for EdgeInsets, Alignment, and BorderRadius without the Directional suffix.

Recommended File Structure

Project Structure
my_flutter_app/
├── assets/
│   └── translations/
│       ├── en.arb          # Source language (English)
│       ├── ar.arb          # Arabic (with 6 plural forms)
│       ├── de.arb          # German
│       ├── ja.arb          # Japanese
│       ├── pt-BR.arb       # Brazilian Portuguese
│       └── pt-PT.arb       # European Portuguese
├── lib/
│   ├── main.dart           # App entry with EasyLocalization
│   ├── app.dart            # MaterialApp with locale delegates
│   └── widgets/
│       └── language_switcher.dart
├── pubspec.yaml            # Dependencies
└── analysis_options.yaml

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Frequently Asked Questions