Home / Blog / Flutter i18n: a complete guide to localization in production apps

Flutter i18n: a complete guide to localization in production apps

Developer resources
Markus Merzinger
Senior Developer

Last updated

7/30/2026

Read time

9 min

Best for

Developers

Illustration of a Flutter localization workflow using an ARB file connected to multiple language variants, highlighting automated translation, localization quality, and efficient multilingual app development with Flutter.

Shipping a Flutter app to more than one market means every string, date, and number has to adapt to the user’s language and region. Flutter i18n covers exactly that: the tooling and file structure that let one codebase serve many locales.

Before we write any code, it helps to answer a common question directly: What is localization in Flutter? It is the process of loading translated content and locale-specific formatting at runtime, driven by the device’s language settings. This guide walks through Flutter localization from a first setup to a production workflow, following the official Flutter internationalization guide and the flutter_localizations and intl packages the framework ships with.

The examples target teams who already know Flutter, Dart, and widget-based architecture. We will skip general programming basics and focus on the localization-specific pieces: delegates, supported locales, ARB files, generated classes, intl formatting, plurals, iOS configuration, and how to keep translations maintainable as the app grows.

Flutter’s own documentation frequently uses the abbreviation l10n for localization. Throughout this guide you will see both Flutter i18n and Flutter l10n, because developers use both terms when they discuss localization workflows. The two fit together: internationalization builds the capability, and localization fills in each language, including layout concerns such as right-to-left text for languages like Arabic and Hebrew.

What is localization in Flutter?

Internationalization (i18n) and localization (l10n) are related but distinct. Internationalization is the engineering work that makes an app capable of adapting to different languages and regions. Localization is the act of providing the actual translated strings and regional formats for each target locale. In practice, you internationalize once by wiring up the framework, then localize repeatedly by adding a new ARB file per language.

Flutter handles both through a generation step. You declare your strings in App Resource Bundle (ARB) files, run a generator, and Flutter produces a typed AppLocalizations class. Your widgets read strings from that class instead of hardcoding text. The framework then picks the right translations based on the active locale.

For anyone comparing app localization Flutter approaches against other frameworks, the notable difference is that Flutter leans on compile-time code generation. You get autocomplete and type checking for every translation key, and a missing key becomes a build-time signal rather than a runtime surprise.

How to use localization in Flutter: project setup

The first step in how to use localization in Flutter is adding the two packages the official stack depends on. flutter_localizations ships with the SDK and provides the framework-level delegates. The intl package provides message formatting and the DateFormat and NumberFormat utilities.

Add them to pubspec.yaml and enable generation:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.20.2

flutter:
  generate: true

With generate: true set, Flutter runs the localization generator during flutter pub get and flutter run. You can also invoke it directly with flutter gen-l10n.

Next, tell the generator where your files live with an l10n.yaml file at the project root:

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

Setting synthetic-package: false writes the generated code into lib/l10n alongside your ARB files, which keeps imports explicit and predictable across the team. The table below maps each piece of the setup to its role.

Piece

File or command

Role

flutter_localizations

pubspec.yaml

Provides Material, Cupertino, and Widgets delegates

intl

pubspec.yaml

Message formatting, DateFormat, NumberFormat

generate: true

pubspec.yaml

Enables ARB-to-Dart generation on build

l10n.yaml

project root

Configures input, template, and output paths

Template ARB

lib/l10n/app_en.arb

Source of truth for keys and placeholders

AppLocalizations

generated app_localizations.dart

Typed accessor for every string

Configuring Flutter i18n delegates and supported locales

Localization delegates are the objects Flutter queries to load localized values for the current locale. The generated AppLocalizations.delegate handles your strings, while the global delegates from flutter_localizations translate built-in widgets such as date pickers and the text selection menu.

Register them in your MaterialApp along with the locales you support:

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

MaterialApp(
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: const [
    Locale('en'),
    Locale('es'),
    Locale('de'),
  ],
  home: const HomePage(),
);

supportedLocales defines the languages your app advertises. When the device locale is not in the list, Flutter falls back to the first entry; order it with your default language first. Once this is in place, any widget can read a string with AppLocalizations.of(context)!.someKey.

Structuring ARB files for maintainable translations

Each locale gets one ARB file, named by its locale code: app_en.arb, app_es.arb, app_de.arb. ARB is a JSON-based format defined by the official ARB specification, with a small metadata convention. Keys hold the translatable text, and a matching key prefixed with @ describes it for translators.

{
  "@@locale": "en",
  "welcomeTitle": "Welcome back",
  "@welcomeTitle": {
    "description": "Heading shown on the home screen after sign-in"
  },
  "greeting": "Hello {userName}",
  "@greeting": {
    "description": "Personalized greeting on the dashboard",
    "placeholders": {
      "userName": { "type": "String" }
    }
  }
}

The @@locale global attribute records which language the file represents. The description fields give translators context, which reduces the ambiguity that produces wrong translations. Placeholders are declared once in the template file with a type, and the generator produces a method that requires the matching argument.

A few habits keep ARB files healthy as the app scales. Keep one template language as the source of truth and generate the others from it. Write descriptive keys such as checkoutPayButton rather than button1. Add a description for every non-obvious string, and never concatenate translated fragments in Dart, because word order differs across languages.

Flutter intl localization: plurals, dates, and numbers

Flutter intl localization covers the cases where a raw string is not enough: counts that change grammar, and dates or numbers that follow regional conventions. ARB files express these with ICU message syntax, the same format the intl package understands.

Pluralization changes the word form based on a number, for example 1 item versus 2 items. ICU handles this inside a single key:

{
  "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" }
    }
  }
}

The generator turns that into AppLocalizations.of(context)!.cartItems(count), and each language file can provide its own plural categories. Which categories a language needs comes from the CLDR plural rules, where English uses one and other while languages such as Arabic and Russian use more. Related is select, which branches on a value such as grammatical gender:

{
  "invitation": "{gender, select, male{He invited you} female{She invited you} other{They invited you}}"
}

Dates and numbers are formatted at runtime with intl, so they respect the active locale without a separate translation:

import 'package:intl/intl.dart';

final locale = Localizations.localeOf(context).toString();
final date = DateFormat.yMMMMd(locale).format(DateTime.now());
final price = NumberFormat.currency(locale: locale, symbol: '€').format(19.99);

The same DateTime renders as “July 20, 2026” for English and “20. Juli 2026” for German, with no branching in your code. Keep formatting in intl and translatable text in ARB, and the two responsibilities stay cleanly separated.

Runtime language switching

Users often expect to change the app language in settings without relaunching. The mechanism is a locale value that MaterialApp listens to, plus persistence so the choice survives restarts. A ValueNotifier<Locale> combined with shared_preferences keeps this lightweight and free of extra state-management dependencies.

class LocaleController {
  static final ValueNotifier<Locale> locale = ValueNotifier(const Locale('en'));

  static Future<void> load() async {
    final prefs = await SharedPreferences.getInstance();
    final code = prefs.getString('locale');
    if (code != null) locale.value = Locale(code);
  }

  static Future<void> set(Locale value) async {
    locale.value = value;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('locale', value.languageCode);
  }
}

Wrap MaterialApp in a ValueListenableBuilder so a change rebuilds the widget tree with the new locale:

ValueListenableBuilder<Locale>(
  valueListenable: LocaleController.locale,
  builder: (context, locale, _) => MaterialApp(
    locale: locale,
    supportedLocales: const [Locale('en'), Locale('es'), Locale('de')],
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    home: const HomePage(),
  ),
);

Call LocaleController.set(const Locale('de')) from a settings screen and the interface updates immediately. Loading the saved value in main() before runApp restores the user’s preference on the next launch.

Flutter localization without context

Some code runs outside the widget tree: background services, exception handlers, and utility functions that format user-facing messages. Flutter localization without context solves the problem that AppLocalizations.of(context) needs a BuildContext you do not always have. The reliable pattern is a global navigator key whose context you reuse.

final navigatorKey = GlobalKey<NavigatorState>();

MaterialApp(
  navigatorKey: navigatorKey,
  // delegates and locales as above
);

// Anywhere, no local context required:
AppLocalizations get tr =>
    AppLocalizations.of(navigatorKey.currentContext!)!;

// Usage in a service:
final message = tr.sessionExpired;

This keeps a single source of translated strings and avoids passing context through layers that should not depend on the widget tree. Guard against a null context during early startup, before the navigator has mounted, by falling back to a default locale where necessary.

Flutter localization on iOS

Flutter localization iOS builds need one extra step beyond the Dart setup. iOS reads supported languages from the app bundle, and the App Store uses that list to show which languages your app offers. Without it, iOS may report only the development region regardless of what your Flutter code supports.

Add CFBundleLocalizations to ios/Runner/Info.plist, listing every locale you ship:

<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
  <string>en</string>
  <string>es</string>
  <string>de</string>
</array>

CFBundleDevelopmentRegion sets the default region, and CFBundleLocalizations declares the full set. Keep this array in sync with supportedLocales in your Dart code; a mismatch is a frequent cause of a language appearing in the app but not on the App Store listing.

Supporting right-to-left languages

Languages such as Arabic, Hebrew, and Persian read right to left, and a localized app mirrors its layout rather than only translating its text. Flutter handles most of this automatically. When the active locale is a right-to-left language, MaterialApp sets the ambient Directionality to TextDirection.rtl, and standard widgets flip their layout to match.

The work on your side is to stop hardcoding a side. Use direction-aware APIs so padding and alignment follow the reading direction:

// Flips automatically in a right-to-left locale:
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
alignment: AlignmentDirectional.centerStart,

Prefer start and end over left and right throughout your widget tree. Icons that imply direction, such as a back arrow, need attention too; wrap them so they mirror with the layout. Test every screen in a right-to-left locale such as Arabic before release, because mirrored layouts surface spacing and alignment issues that left-to-right testing never reveals.

Scaling Flutter localization with a team workflow

Manual ARB editing works for a handful of strings in one or two languages. It stops working once several developers add keys every sprint and translators handle a dozen locales. At that point the bottleneck moves from writing code to coordinating people, keeping files structurally aligned, and catching errors before they reach users.

LingoHub connects this workflow to your repository. It reads your ARB files directly, and its Flutter/ARB integration preserves the @ descriptions, @@locale globals, and x- custom attributes on import and export, which keeps the generated Dart output intact. Placeholders in {name} form and ICU plurals are recognized as structured content rather than plain text, which lets translators work on all plural forms of a string together.

The Git-based connection pulls new and changed keys automatically and opens a pull request when translations are ready, so localization tracks development instead of lagging behind it. Automated quality checks catch mismatched placeholders, missing translations, and length overflows before merge. A shared glossary, translation memory, and AI translation with human review keep terminology consistent across every locale, while developers, translators, and reviewers collaborate in one place. For mobile teams specifically, the mobile app localization use case covers store metadata and cross-platform releases alongside the in-app strings.

Flutter localization best practices

These Flutter localization best practices come up repeatedly in production apps, and the table pairs each common mistake with the symptom and the fix.

Mistake

What you see

Fix

Concatenating translated fragments

Wrong word order in some languages

Use a single ICU string with placeholders

Missing description metadata

Ambiguous or incorrect translations

Add @key descriptions for every non-obvious string

Locale list out of sync with Info.plist

Language missing from App Store listing

Match CFBundleLocalizations to supportedLocales

Formatting dates or numbers manually

Regional formats ignored

Use DateFormat and NumberFormat from intl

Editing target ARB files by hand at scale

Structural drift between locales

Manage translations in a platform that aligns keys automatically

Beyond the table, keep the template language complete before translation starts, since translators work from it. Run flutter gen-l10n in continuous integration so a missing key fails the build early. Budget for text expansion, because languages such as German often run longer than English and can overflow fixed-width layouts.

Frequently asked questions

What is localization in Flutter, in one sentence?

It is loading translated strings and locale-specific date and number formats at runtime, driven by the device language, using ARB files and the generated AppLocalizations class.

What is the best app localization Flutter setup for production?

The official stack of flutter_localizations and intl with ARB files and flutter gen-l10n is the recommended baseline, paired with a translation management platform once you support several languages and multiple contributors.

How do I handle Flutter localization without context?

Expose a global GlobalKey<NavigatorState>, then read AppLocalizations.of(navigatorKey.currentContext!) from services and handlers that have no BuildContext of their own.

Why does my language show in the app but not on iOS?

The locale is likely missing from CFBundleLocalizations in ios/Runner/Info.plist. iOS and the App Store read supported languages from that array, so it must match your Dart supportedLocales.

How does pluralization work in Flutter intl localization?

You write one ICU message with plural categories inside a single ARB key, and the generator produces a method that takes the count and returns the correct form for the active locale.

Conclusion

Flutter i18n gives you a typed, generation-based path from a single template ARB file to a fully localized production app: delegates and supported locales for the framework, ARB and ICU for content, intl for formatting, and dedicated patterns for language switching, context-free access, and iOS. The engineering scales cleanly; the coordination is what gets harder as languages and contributors multiply.

Want to see ARB-based Flutter localization run as an automated, collaborative workflow with your repository? Try the app for free or book a LingoHub demo and walk through it with your own project.

Related articles