Flutter Localization with ARB Files: A Complete, Modern Tutorial

A step-by-step Flutter localization tutorial using ARB files and gen‑l10n: setup, ARB authoring, plurals, selects, runtime switching, testing, and CI.

ASOasis
6 min read
Flutter Localization with ARB Files: A Complete, Modern Tutorial

Image used for representation purposes only.

Overview

Flutter’s built‑in localization toolchain makes translating apps straightforward. At the center of this workflow are ARB (Application Resource Bundle) files—JSON-like files that pair message keys with localized strings and rich metadata. In this tutorial, you’ll learn how to

  • configure Flutter’s gen‑l10n tool
  • author ARB files with placeholders, plurals, and selects
  • generate strongly typed localization code
  • wire it into MaterialApp/CupertinoApp
  • test, validate, and scale your localization process

The result is a clean, type‑safe, translator‑friendly setup you can trust in production.

What are ARB files?

ARB is a simple, translator‑friendly format built on JSON. Each message is a key/value pair, and each message can include a companion metadata entry (prefixed with @) containing descriptions and placeholder info. Example:

{
  "greeting": "Hello {name}",
  "@greeting": {
    "description": "Friendly greeting shown on the home screen.",
    "placeholders": {
      "name": { "type": "String", "example": "Amina" }
    }
  }
}

Flutter’s gen‑l10n tool reads your ARB files, validates them, and generates a strongly typed Dart API (e.g., AppLocalizations.greeting(name)) that you can call from your widgets.

Project setup

1) Add dependencies

In pubspec.yaml, add Flutter’s localization package (and intl if you’ll format dates/numbers yourself):

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0 # optional but useful for custom formatting

Run:

flutter pub get

2) Enable localization code generation

Turn on code generation in pubspec.yaml:

flutter:
  generate: true

3) (Optional) Create l10n.yaml

l10n.yaml lets you customize the tool. Create it at the project root:

# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: true
untranslated-messages-file: build/l10n_missing.txt
use-deferred-loading: false
  • arb-dir: where your ARB files live
  • template-arb-file: your source locale (usually English) that defines message keys
  • synthetic-package: true lets you import from package:flutter_gen/…
  • untranslated-messages-file: where missing keys are reported

If you skip l10n.yaml, Flutter uses sensible defaults (lib/l10n, etc.).

Author your ARB files

Create lib/l10n/app_en.arb (the template) and at least one translation, e.g., lib/l10n/app_es.arb.

app_en.arb:

{
  "appTitle": "Caffeine Tracker",
  "greeting": "Hello {name}",
  "@greeting": {
    "description": "Personalized greeting on the dashboard.",
    "placeholders": {
      "name": { "type": "String", "example": "Kai" }
    }
  },
  "cupsToday": "{count, plural, =0{No cups today} =1{One cup today} other{{count} cups today}}",
  "@cupsToday": {
    "description": "Shows how many cups were logged today.",
    "placeholders": {
      "count": { "type": "int", "example": 3 }
    }
  },
  "userRole": "{role, select, admin{Administrator} editor{Editor} viewer{Viewer} other{User}}",
  "@userRole": {
    "description": "Maps a backend role to human‑readable text.",
    "placeholders": {
      "role": { "type": "String", "example": "admin" }
    }
  },
  "dueDate": "Due on {date}",
  "@dueDate": {
    "description": "Task due date.",
    "placeholders": {
      "date": { "type": "DateTime", "example": "2026-04-16T10:00:00Z" }
    }
  }
}

app_es.arb:

{
  "appTitle": "Contador de Cafeína",
  "greeting": "Hola {name}",
  "cupsToday": "{count, plural, =0{Ninguna taza hoy} =1{Una taza hoy} other{{count} tazas hoy}}",
  "userRole": "{role, select, admin{Administrador} editor{Editor} viewer{Espectador} other{Usuario}}",
  "dueDate": "Vence el {date}"
}

Guidelines:

  • Keep IDs stable across locales; never translate the keys.
  • Provide rich descriptions and placeholder examples to help translators.
  • Use ICU MessageFormat for plurals and selects.
  • The English ARB defines the canonical set of messages. Other locales should contain the same keys.

Generate the localization code

Run the generator:

flutter gen-l10n

This produces a strongly typed API (by default at package:flutter_gen/gen_l10n/app_localizations.dart via a synthetic package). Do not commit generated files if you rely on synthetic packages; they’re regenerated per build.

Wire it into your app

Update your root app widget:

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(l10n.greeting('Amina')),
            const SizedBox(height: 8),
            Text(l10n.cupsToday(3)),
            const SizedBox(height: 8),
            Text(l10n.userRole('admin')),
            const SizedBox(height: 8),
            Text(l10n.dueDate(DateTime.now())),
          ],
        ),
      ),
    );
  }
}

That’s it—your app now reads localized strings from ARB files and responds to the device locale automatically.

Switching locales at runtime

By default, Flutter uses the device locale. To let users pick a language in‑app, expose a locale state and pass it into MaterialApp.locale.

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Locale? _locale;

  void _setLocale(Locale? newLocale) => setState(() => _locale = newLocale);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      locale: _locale,
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: SettingsPage(onLocaleChanged: _setLocale),
    );
  }
}

class SettingsPage extends StatelessWidget {
  final void Function(Locale?) onLocaleChanged;
  const SettingsPage({super.key, required this.onLocaleChanged});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(AppLocalizations.of(context)!.appTitle)),
      body: DropdownButton<Locale?>(
        value: Localizations.localeOf(context),
        items: AppLocalizations.supportedLocales
            .map((l) => DropdownMenuItem(value: l, child: Text(l.toLanguageTag())))
            .toList(),
        onChanged: onLocaleChanged,
      ),
    );
  }
}

Dates and numbers

The generator understands DateTime and numeric placeholders. For custom formats, you can still use intl directly:

import 'package:intl/intl.dart';

final amount = 1234.56;
final formatted = NumberFormat.currency(locale: Localizations.localeOf(context).toLanguageTag()).format(amount);

If you rely on ARB placeholders with DateTime or numbers, the generated methods will apply locale‑aware formatting by default. Use descriptions and examples in @metadata to guide translators.

Validation and quality checks

  • Run flutter gen-l10n regularly to catch missing or malformed messages.
  • Inspect build/l10n_missing.txt (as configured) for untranslated keys.
  • Add a CI step:
flutter gen-l10n --fail-on-warnings
flutter analyze
flutter test
  • Consider adding a “pseudo‑locale” (e.g., en_XA) to visually expand text and surface clipping/overflow issues during QA.

Testing localized widgets

Widget test example that forces a locale:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

void main() {
  testWidgets('Spanish greeting is shown', (tester) async {
    await tester.pumpWidget(MaterialApp(
      locale: const Locale('es'),
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: Builder(
        builder: (context) => Text(AppLocalizations.of(context)!.greeting('Kai')),
      ),
    ));

    expect(find.text('Hola Kai'), findsOneWidget);
  });
}

RTL, fonts, and layout

  • Flutter automatically handles directionality for RTL locales (e.g., Arabic, Hebrew) when GlobalWidgetsLocalizations is included.
  • Verify iconography, paddings, and animations in RTL.
  • Ensure your font supports target scripts; consider dynamic font fallback or locale‑specific fonts where appropriate.

Organizing and scaling translations

  • Keep messages short, clear, and reusable.
  • Use one ARB per locale: app_en.arb, app_es.arb, app_ar.arb, etc.
  • Group related keys with prefixes (home_, settings_, error_…).
  • Provide descriptions and examples for every placeholder.
  • Share ARB files with translators via your localization platform (many tools import/export ARB directly).

Common pitfalls (and fixes)

  • Missing key in a locale: run flutter gen-l10n and check the missing report; add the key to all locales.
  • Placeholder mismatches: make sure placeholder names and counts match across locales.
  • Plural/select logic errors: test with various values; remember ICU syntax must be exact.
  • Hard‑coded strings left in widgets: search for raw strings and move them into ARB.
  • Forgetting delegates/supportedLocales: ensure both are present in MaterialApp/CupertinoApp.

Handy checklist

  • Add flutter_localizations and enable generate: true
  • Create l10n.yaml (optional) and ARB files in lib/l10n
  • Define messages, placeholders, plurals/selects, and descriptions
  • Run flutter gen-l10n
  • Import AppLocalizations and wire delegates/supportedLocales
  • Replace hard‑coded strings with l10n getters
  • Test with multiple locales (including pseudo‑locale)
  • Automate checks in CI

Conclusion

ARB‑driven localization with gen‑l10n gives you a maintainable, type‑safe workflow that scales from MVP to enterprise. With well‑structured ARB files, precise metadata, and automated validation, you’ll deliver a polished, truly global Flutter app with minimal friction.

Related Posts