IntlPull
Tutorial
13 min read

Flutter i18n: Complete Guide to App Localization in 2026

Master Flutter internationalization with ARB files, gen-l10n, and IntlPull CLI. Learn plurals, gender support, platform channels, testing strategies, and OTA updates for production-ready multilingual apps.

IntlPull Team
IntlPull Team
20 Feb 2026, 01:39 PM [PST]
On this page
Summary

Master Flutter internationalization with ARB files, gen-l10n, and IntlPull CLI. Learn plurals, gender support, platform channels, testing strategies, and OTA updates for production-ready multilingual apps.

Flutter's cross-platform architecture makes it an excellent choice for building apps that reach global audiences. With first-class internationalization (i18n) support through the flutter_localizations package and ARB (Application Resource Bundle) files, Flutter provides a robust foundation for multilingual apps. This comprehensive guide covers everything from basic setup to advanced patterns like plural handling, gender support, platform-specific strings, and integration with modern translation management systems like IntlPull for streamlined workflows and over-the-air (OTA) updates.

Understanding Flutter's Localization Architecture

Flutter's i18n system is built around three core components:

  1. ARB Files: JSON-based resource bundles that store translations with metadata
  2. gen-l10n Tool: Code generator that creates type-safe Dart classes from ARB files
  3. flutter_localizations Package: Framework integration for locale detection and fallback

This architecture ensures type safety, prevents missing translation errors at compile time, and provides a developer-friendly API. Unlike manual string interpolation, Flutter's approach generates strongly-typed methods with IDE autocomplete support.

Setting Up flutter_localizations

Step 1: Add Dependencies

Update your pubspec.yaml:

YAML
1dependencies:
2  flutter:
3    sdk: flutter
4  flutter_localizations:
5    sdk: flutter
6  intl: ^0.19.0
7
8flutter:
9  generate: true

Step 2: Configure l10n.yaml

Create l10n.yaml in your project root:

YAML
1arb-dir: lib/l10n
2template-arb-file: app_en.arb
3output-localization-file: app_localizations.dart
4output-class: AppLocalizations
5nullable-getter: false

Configuration Options:

  • arb-dir: Directory containing ARB files
  • template-arb-file: Source language file (usually English)
  • output-localization-file: Generated Dart file name
  • nullable-getter: Set to false to avoid null checks

Step 3: Create ARB Files

Create lib/l10n/app_en.arb:

JSON
1{
2  "@@locale": "en",
3  "appTitle": "My App",
4  "@appTitle": {
5    "description": "Application title shown in app bar"
6  },
7  "welcomeMessage": "Welcome, {userName}!",
8  "@welcomeMessage": {
9    "description": "Greeting shown on home screen",
10    "placeholders": {
11      "userName": {
12        "type": "String",
13        "example": "Alice"
14      }
15    }
16  },
17  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
18  "@itemCount": {
19    "description": "Item count with plural support",
20    "placeholders": {
21      "count": {
22        "type": "int",
23        "format": "compact"
24      }
25    }
26  }
27}

Create lib/l10n/app_es.arb:

JSON
1{
2  "@@locale": "es",
3  "appTitle": "Mi Aplicación",
4  "welcomeMessage": "¡Bienvenido, {userName}!",
5  "itemCount": "{count, plural, =0{Sin artículos} =1{1 artículo} other{{count} artículos}}"
6}

Step 4: Generate Localization Code

Run code generation:

Terminal
flutter gen-l10n

This creates .dart_tool/flutter_gen/gen_l10n/ with type-safe classes.

Step 5: Configure MaterialApp

Update lib/main.dart:

DART
1import 'package:flutter/material.dart';
2import 'package:flutter_localizations/flutter_localizations.dart';
3import 'package:flutter_gen/gen_l10n/app_localizations.dart';
4
5void main() {
6  runApp(const MyApp());
7}
8
9class MyApp extends StatelessWidget {
10  const MyApp({Key? key}) : super(key: key);
11
12  
13  Widget build(BuildContext context) {
14    return MaterialApp(
15      title: 'Flutter i18n Demo',
16      localizationsDelegates: const [
17        AppLocalizations.delegate,
18        GlobalMaterialLocalizations.delegate,
19        GlobalWidgetsLocalizations.delegate,
20        GlobalCupertinoLocalizations.delegate,
21      ],
22      supportedLocales: const [
23        Locale('en'), // English
24        Locale('es'), // Spanish
25        Locale('fr'), // French
26      ],
27      home: const HomeScreen(),
28    );
29  }
30}

Step 6: Use Translations in Widgets

DART
1class HomeScreen extends StatelessWidget {
2  const HomeScreen({Key? key}) : super(key: key);
3
4  
5  Widget build(BuildContext context) {
6    final l10n = AppLocalizations.of(context)!;
7
8    return Scaffold(
9      appBar: AppBar(
10        title: Text(l10n.appTitle),
11      ),
12      body: Center(
13        child: Column(
14          mainAxisAlignment: MainAxisAlignment.center,
15          children: [
16            Text(l10n.welcomeMessage('Alice')),
17            const SizedBox(height: 20),
18            Text(l10n.itemCount(0)),
19            Text(l10n.itemCount(1)),
20            Text(l10n.itemCount(5)),
21          ],
22        ),
23      ),
24    );
25  }
26}

Advanced ARB File Features

Plurals and Selects

Flutter supports ICU MessageFormat for complex grammar:

JSON
1{
2  "newMessages": "{count, plural, =0{No new messages} =1{1 new message} other{{count} new messages}}",
3  "gender": "{userGender, select, male{He replied} female{She replied} other{They replied}}"
4}

Usage in Dart:

DART
Text(l10n.newMessages(5)); // "5 new messages"
Text(l10n.gender('female')); // "She replied"

Date and Number Formatting

JSON
1{
2  "lastUpdated": "Last updated: {date}",
3  "@lastUpdated": {
4    "placeholders": {
5      "date": {
6        "type": "DateTime",
7        "format": "yMMMMd"
8      }
9    }
10  },
11  "price": "Price: {amount}",
12  "@price": {
13    "placeholders": {
14      "amount": {
15        "type": "double",
16        "format": "currency",
17        "optionalParameters": {
18          "symbol": "$"
19        }
20      }
21    }
22  }
23}

Usage:

DART
Text(l10n.lastUpdated(DateTime.now())); // "Last updated: February 12, 2026"
Text(l10n.price(19.99)); // "Price: $19.99"

Nested Placeholders

JSON
{
  "greeting": "Hello {name}, you have {count, plural, =0{no messages} =1{1 message} other{{count} messages}}"
}

Platform-Specific Translations

iOS vs Android String Differences

Create platform-aware translations:

JSON
1{
2  "tapAction": "{platform, select, ios{Tap} android{Tap} other{Click}}",
3  "shareAction": "{platform, select, ios{Share...} android{Share} other{Share}}"
4}

Detect Platform in Dart:

DART
1import 'dart:io';
2
3String getPlatform() {
4  if (Platform.isIOS) return 'ios';
5  if (Platform.isAndroid) return 'android';
6  return 'other';
7}
8
9// Usage
10Text(l10n.tapAction(getPlatform()));

Platform Channels for Native Strings

For strings managed by native code:

DART
1import 'package:flutter/services.dart';
2
3class NativeLocalizations {
4  static const MethodChannel _channel = MethodChannel('app.localization');
5
6  static Future<String> getString(String key) async {
7    try {
8      final String result = await _channel.invokeMethod('getString', {'key': key});
9      return result;
10    } catch (e) {
11      return key; // Fallback to key
12    }
13  }
14}

iOS Implementation (Swift):

Swift
1FlutterMethodChannel(name: "app.localization", binaryMessenger: controller.binaryMessenger)
2  .setMethodCallHandler { call, result in
3    if call.method == "getString", let key = call.arguments as? String {
4      result(NSLocalizedString(key, comment: ""))
5    } else {
6      result(FlutterMethodNotImplemented)
7    }
8  }

IntlPull CLI Integration

IntlPull streamlines Flutter localization with automated workflows and OTA updates.

Installation and Setup

Terminal
1# Install CLI globally
2npm install -g @intlpullhq/cli
3
4# Initialize in Flutter project
5cd my_flutter_app
6intlpull init --framework flutter

Configuration (intlpull.config.json):

JSON
1{
2  "projectId": "your-project-id",
3  "apiKey": "ip_live_...",
4  "framework": "flutter",
5  "sourcePath": "lib/l10n",
6  "format": "arb",
7  "languages": ["en", "es", "fr", "de"],
8  "defaultLanguage": "en"
9}

Automated ARB Extraction

Terminal
1# Scan Dart code for hardcoded strings
2intlpull scan --auto-wrap
3
4# Before:
5Text('Welcome to our app')
6
7# After:
8Text(AppLocalizations.of(context)!.welcome)

Push and Pull Translations

Terminal
1# Upload ARB files to IntlPull
2intlpull push
3
4# Download latest translations
5intlpull pull
6
7# Watch for changes (real-time sync)
8intlpull watch

OTA Updates for Flutter

IntlPull's OTA SDK enables instant translation updates without app resubmission:

Terminal
# Add dependency
flutter pub add intlpull_ota

Integration:

DART
1import 'package:intlpull_ota/intlpull_ota.dart';
2
3void main() async {
4  WidgetsFlutterBinding.ensureInitialized();
5
6  final ota = IntlPullOTA(
7    projectId: 'your-project-id',
8    apiKey: 'ip_live_...',
9    environment: 'production',
10  );
11
12  await ota.initialize();
13
14  runApp(MyApp(ota: ota));
15}
16
17class MyApp extends StatelessWidget {
18  final IntlPullOTA ota;
19  const MyApp({required this.ota, Key? key}) : super(key: key);
20
21  
22  Widget build(BuildContext context) {
23    return OTAProvider(
24      ota: ota,
25      child: MaterialApp(
26        localizationsDelegates: [
27          OTALocalizationsDelegate(ota),
28          AppLocalizations.delegate,
29          // ...
30        ],
31        // ...
32      ),
33    );
34  }
35}

OTA Workflow:

Terminal
# Publish new release from CLI
intlpull publish --version 1.2.0 --message "Fixed typos"

App fetches updates on launch (cached for 24 hours). Critical fixes deploy in minutes instead of days.

Testing Localization

Unit Tests for Translations

DART
1import 'package:flutter_test/flutter_test.dart';
2import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3
4void main() {
5  test('Spanish translations exist', () async {
6    final localizations = await AppLocalizations.delegate.load(const Locale('es'));
7    expect(localizations.appTitle, 'Mi Aplicación');
8    expect(localizations.welcomeMessage('Carlos'), contains('Carlos'));
9  });
10
11  test('Plural forms work correctly', () async {
12    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
13    expect(localizations.itemCount(0), 'No items');
14    expect(localizations.itemCount(1), '1 item');
15    expect(localizations.itemCount(5), '5 items');
16  });
17}

Widget Tests

DART
1testWidgets('Home screen shows translated title', (tester) async {
2  await tester.pumpWidget(
3    MaterialApp(
4      localizationsDelegates: [
5        AppLocalizations.delegate,
6        GlobalMaterialLocalizations.delegate,
7      ],
8      supportedLocales: const [Locale('en')],
9      home: const HomeScreen(),
10    ),
11  );
12
13  expect(find.text('My App'), findsOneWidget);
14});

Manual Testing with Device Locale

Change device language in simulator/emulator:

iOS Simulator:

Settings → General → Language & Region → Add Language

Android Emulator:

Settings → System → Languages & input → Languages → Add a language

Golden Tests for Layout

Test UI doesn't break with longer translations:

DART
1testWidgets('Button fits Spanish translation', (tester) async {
2  await tester.pumpWidget(/* Spanish locale widget */);
3  await expectLater(
4    find.byType(MaterialApp),
5    matchesGoldenFile('golden/button_es.png'),
6  );
7});

Best Practices and Performance

1. Avoid Runtime Translation Lookups

Bad:

DART
// Don't dynamically construct keys
Text(l10n['button_' + action]); // Won't compile

Good:

DART
1// Use explicit methods
2String getActionText(String action) {
3  switch (action) {
4    case 'save': return l10n.saveButton;
5    case 'cancel': return l10n.cancelButton;
6    default: return l10n.defaultButton;
7  }
8}

2. Lazy Load ARB Files

For large apps, split ARB files by feature:

YAML
1# l10n.yaml
2arb-dir: lib/l10n
3template-arb-file: app_en.arb
4output-localization-file: app_localizations.dart
5synthetic-package: false

Create separate ARB files:

  • app_en.arb (global strings)
  • home_en.arb (home screen)
  • settings_en.arb (settings)

3. Cache Localizations in State

DART
1class HomeScreen extends StatefulWidget {
2  
3  State<HomeScreen> createState() => _HomeScreenState();
4}
5
6class _HomeScreenState extends State<HomeScreen> {
7  late final AppLocalizations l10n;
8
9  
10  void didChangeDependencies() {
11    super.didChangeDependencies();
12    l10n = AppLocalizations.of(context)!;
13  }
14
15  
16  Widget build(BuildContext context) {
17    return Text(l10n.appTitle); // Cached lookup
18  }
19}

4. Use intl_utils for Tooling

Install intl_utils for better DX:

Terminal
flutter pub global activate intl_utils

Add to pubspec.yaml:

YAML
flutter_intl:
  enabled: true

5. Validate ARB Files in CI/CD

Terminal
1# Check for missing translations
2intlpull validate --missing
3
4# Check for unused keys
5intlpull validate --unused
6
7# Run in CI
8- name: Validate Translations
9  run: |
10    npm install -g @intlpullhq/cli
11    intlpull validate --missing --unused --fail-on-error

Common Pitfalls and Solutions

Issue 1: "Locale Not Found" Error

Cause: Locale declared in supportedLocales but ARB file missing.

Solution:

DART
1supportedLocales: const [
2  Locale('en'),
3  Locale('es'),
4  // Don't add locales without ARB files
5],

Issue 2: Plural Forms Not Working

Cause: Incorrect ICU MessageFormat syntax.

Solution:

JSON
1// Wrong
2"items": "{count} items"
3
4// Correct
5"items": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"

Issue 3: Hot Reload Doesn't Update Translations

Cause: Generated code not refreshed.

Solution:

Terminal
1# Regenerate localizations
2flutter gen-l10n
3
4# Full restart required (not hot reload)
5flutter run

Issue 4: RTL Layout Issues

Solution: Test with RTL languages (Arabic, Hebrew):

DART
1MaterialApp(
2  localizationsDelegates: [...],
3  supportedLocales: [
4    const Locale('en'),
5    const Locale('ar'), // Right-to-left
6  ],
7  builder: (context, child) {
8    return Directionality(
9      textDirection: Localizations.localeOf(context).languageCode == 'ar'
10          ? TextDirection.rtl
11          : TextDirection.ltr,
12      child: child!,
13    );
14  },
15)

Production Deployment Checklist

  • All ARB files contain same keys
  • Plural forms tested for all languages
  • Date/number formats verified per locale
  • RTL layouts tested (if supporting Arabic/Hebrew)
  • Platform-specific strings validated on iOS and Android
  • IntlPull OTA SDK configured for production
  • CI/CD validates translations before merge
  • Fallback locale configured (en recommended)
  • Translation memory populated for reuse
  • Team trained on ARB file editing

Frequently Asked Questions

How do I handle region-specific locales (e.g., en_US vs en_GB)?

Use full locale codes in ARB files:

lib/l10n/app_en_US.arb
lib/l10n/app_en_GB.arb
DART
1supportedLocales: const [
2  Locale('en', 'US'),
3  Locale('en', 'GB'),
4],

Can I use JSON instead of ARB files?

ARB is required for gen-l10n. However, IntlPull CLI can convert JSON to ARB:

Terminal
intlpull convert --from json --to arb --input translations/ --output lib/l10n/

How do I translate app name and permissions?

App names require platform-specific configuration:

iOS (ios/Runner/Info.plist):

XML
<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>

Android (android/app/src/main/res/values-es/strings.xml):

XML
<string name="app_name">Mi Aplicación</string>

What's the performance impact of many translations?

Minimal. Flutter loads only active locale. For 10,000+ keys, consider namespace splitting.

How do I test translations without changing device locale?

Force locale programmatically:

DART
1MaterialApp(
2  locale: const Locale('es'), // Force Spanish
3  localizationsDelegates: [...],
4)

Can I use IntlPull with existing ARB files?

Yes. Run intlpull init in your Flutter project, and existing ARB files will be detected and imported.

How often should I publish OTA updates?

  • Critical fixes (typos in checkout): Immediately
  • New features: With app releases
  • Seasonal campaigns: As needed
  • A/B tests: Daily/weekly

Recommended cadence: Weekly maintenance releases, immediate for critical bugs.

Conclusion

Flutter's i18n architecture provides a robust foundation for building multilingual apps that scale globally. By leveraging ARB files, the gen-l10n tool, and modern translation management platforms like IntlPull, you can reduce localization overhead by 80% while delivering instant updates to users. The type-safe, compile-time validation ensures translation bugs are caught before production, and OTA updates eliminate the painful app store submission cycle for translation fixes.

Start with the basics (ARB files + flutter_localizations), gradually adopt advanced features (plurals, platform overrides), and integrate IntlPull CLI for team collaboration and automated workflows. Your users across the globe will appreciate the attention to linguistic detail, and your development team will appreciate the time saved.

Ready to streamline your Flutter localization workflow? Try IntlPull free with 500 keys and 3 languages, or explore our Flutter-specific documentation for more advanced patterns.

Tags
flutter
dart
i18n
localization
mobile
arb
intl
cross-platform
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.