IntlPull
Tutorial
15 min read

How to Internationalize Your Flutter App with ARB Files: Complete Tutorial

Learn how to internationalize your Flutter app using ARB files and the Intl package. Step-by-step guide covering setup, plurals, gender forms, date formatting, and streamlined workflows with IntlPull CLI for production-ready multilingual apps.

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

Learn how to internationalize your Flutter app using ARB files and the Intl package. Step-by-step guide covering setup, plurals, gender forms, date formatting, and streamlined workflows with IntlPull CLI for production-ready multilingual apps.

Building a Flutter app that reaches users worldwide requires proper internationalization (i18n). While Flutter provides excellent i18n support out of the box, understanding how to effectively use ARB (Application Resource Bundle) files can be the difference between a clunky localization workflow and a streamlined, maintainable system. This comprehensive tutorial walks you through everything you need to know about internationalizing your Flutter app with ARB files, from basic setup to advanced patterns and modern tooling with IntlPull.

What Are ARB Files and Why Use Them?

ARB (Application Resource Bundle) is a JSON-based format specifically designed for storing localizable resources. Originally created by Google, ARB files have become the standard for Flutter internationalization because they offer several key advantages:

Type Safety: ARB files work with Flutter's code generation to create type-safe translation methods, catching errors at compile time rather than runtime.

Rich Metadata: Unlike plain JSON, ARB files support metadata for each translation, including descriptions, placeholders, and examples that help translators understand context.

ICU MessageFormat Support: Built-in support for complex grammar rules like plurals, gender forms, and conditional text.

Tooling Integration: ARB files integrate seamlessly with translation management systems, making team collaboration effortless.

Here's what a basic ARB file looks like:

JSON
1{
2  "@@locale": "en",
3  "helloWorld": "Hello World!",
4  "@helloWorld": {
5    "description": "The conventional newborn programmer greeting"
6  }
7}

The @@locale key identifies the language, helloWorld is your translation key, and @helloWorld contains metadata that helps translators understand the context.

Setting Up Flutter Internationalization

Let's build a complete internationalization setup from scratch. We'll create a simple app that demonstrates all the key concepts.

Step 1: Add Required Dependencies

First, update your pubspec.yaml to include the necessary packages:

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

The flutter_localizations package provides localization support for Material and Cupertino widgets, while intl handles message formatting, date/time formatting, and number formatting.

Step 2: Configure Code Generation

Create a file named l10n.yaml in your project root:

YAML
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

This configuration tells Flutter where to find your ARB files and where to generate the localization code.

Step 3: Create Your First ARB File

Create the directory lib/l10n and add your template file app_en.arb:

JSON
1{
2  "@@locale": "en",
3  "appTitle": "My Awesome App",
4  "@appTitle": {
5    "description": "The title of the application"
6  },
7  "welcomeMessage": "Welcome to our app!",
8  "@welcomeMessage": {
9    "description": "Message shown on the home screen"
10  },
11  "greeting": "Hello, {name}!",
12  "@greeting": {
13    "description": "Personalized greeting",
14    "placeholders": {
15      "name": {
16        "type": "String",
17        "example": "Alice"
18      }
19    }
20  }
21}

Step 4: Add Additional Languages

Create app_es.arb for Spanish:

JSON
1{
2  "@@locale": "es",
3  "appTitle": "Mi Aplicación Increíble",
4  "welcomeMessage": "¡Bienvenido a nuestra aplicación!",
5  "greeting": "¡Hola, {name}!"
6}

Notice that you only need to include the translations, not the metadata. The metadata from the template file is automatically used.

Step 5: Generate Localization Code

Run the code generation command:

Terminal
flutter gen-l10n

This creates type-safe Dart classes in .dart_tool/flutter_gen/gen_l10n/ that you'll use to access your translations.

Step 6: Configure Your App

Update your main.dart to enable localization:

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({super.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'),
24        Locale('es'),
25      ],
26      home: const HomeScreen(),
27    );
28  }
29}

Step 7: Use Translations in Your Widgets

Now you can access translations in any widget:

DART
1class HomeScreen extends StatelessWidget {
2  const HomeScreen({super.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(
17              l10n.welcomeMessage,
18              style: Theme.of(context).textTheme.headlineMedium,
19            ),
20            const SizedBox(height: 20),
21            Text(l10n.greeting('Alice')),
22          ],
23        ),
24      ),
25    );
26  }
27}

That's it! Your app now supports multiple languages. The device's locale will automatically determine which language to display.

Handling Plurals and Gender Forms

One of the most powerful features of ARB files is support for ICU MessageFormat, which handles complex grammar rules that vary across languages.

Plural Forms

Different languages have different plural rules. English has two forms (one/other), but Polish has three, and Arabic has six. ARB files handle this automatically:

JSON
1{
2  "itemCount": "{count, plural, =0{No items} =1{One item} other{{count} items}}",
3  "@itemCount": {
4    "description": "Number of items in the cart",
5    "placeholders": {
6      "count": {
7        "type": "int"
8      }
9    }
10  }
11}

In Spanish (app_es.arb):

JSON
{
  "itemCount": "{count, plural, =0{Sin artículos} =1{Un artículo} other{{count} artículos}}"
}

Usage in Dart:

DART
Text(l10n.itemCount(0)); // "No items"
Text(l10n.itemCount(1)); // "One item"
Text(l10n.itemCount(5)); // "5 items"

Gender Forms

Some languages require different text based on gender:

JSON
1{
2  "replyMessage": "{gender, select, male{He replied to your message} female{She replied to your message} other{They replied to your message}}",
3  "@replyMessage": {
4    "description": "Notification message for replies",
5    "placeholders": {
6      "gender": {
7        "type": "String"
8      }
9    }
10  }
11}

Usage:

DART
Text(l10n.replyMessage('female')); // "She replied to your message"

Combining Plurals and Gender

You can nest these patterns for complex scenarios:

JSON
1{
2  "friendRequest": "{count, plural, =1{{gender, select, male{He} female{She} other{They}} sent you a friend request} other{{count} people sent you friend requests}}",
3  "@friendRequest": {
4    "placeholders": {
5      "count": {"type": "int"},
6      "gender": {"type": "String"}
7    }
8  }
9}

Date, Time, and Number Formatting

ARB files support locale-aware formatting for dates, times, and numbers.

Date Formatting

JSON
1{
2  "lastSeen": "Last seen: {date}",
3  "@lastSeen": {
4    "placeholders": {
5      "date": {
6        "type": "DateTime",
7        "format": "yMMMd"
8      }
9    }
10  }
11}

Common date formats:

  • yMMMd: Feb 12, 2026
  • yMMMMd: February 12, 2026
  • jm: 5:30 PM
  • yMd: 2/12/2026

Usage:

DART
Text(l10n.lastSeen(DateTime.now()));
// English: "Last seen: Feb 12, 2026"
// Spanish: "Last seen: 12 feb 2026"

Number and Currency Formatting

JSON
1{
2  "price": "Price: {amount}",
3  "@price": {
4    "placeholders": {
5      "amount": {
6        "type": "double",
7        "format": "currency",
8        "optionalParameters": {
9          "symbol": "$",
10          "decimalDigits": 2
11        }
12      }
13    }
14  },
15  "discount": "Save {percent}%",
16  "@discount": {
17    "placeholders": {
18      "percent": {
19        "type": "int",
20        "format": "compact"
21      }
22    }
23  }
24}

Usage:

DART
Text(l10n.price(29.99)); // "Price: $29.99"
Text(l10n.discount(15)); // "Save 15%"

Organizing Large Translation Files

As your app grows, managing translations becomes more complex. Here are strategies for keeping things organized.

Namespace by Feature

Instead of one massive ARB file, split translations by feature:

lib/l10n/
  ├── app_en.arb          # Global strings
  ├── auth_en.arb         # Authentication
  ├── profile_en.arb      # User profile
  ├── settings_en.arb     # Settings
  └── ...

Update l10n.yaml:

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

Use Descriptive Key Names

Bad:

JSON
1{
2  "btn1": "Submit",
3  "msg2": "Error occurred"
4}

Good:

JSON
1{
2  "submitButton": "Submit",
3  "networkErrorMessage": "Error occurred"
4}

Add Context with Metadata

Always include descriptions and examples:

JSON
1{
2  "tapToEdit": "Tap to edit",
3  "@tapToEdit": {
4    "description": "Hint text shown below editable fields",
5    "context": "Used in profile screen for bio field"
6  }
7}

This helps translators understand where and how the text is used, leading to better translations.

Streamlining Workflows with IntlPull

Managing ARB files manually works for small projects, but as your team and translation count grow, you need better tooling. IntlPull provides a complete translation management platform specifically designed for developers.

Why IntlPull for Flutter?

Native ARB Support: IntlPull understands ARB file structure, including ICU MessageFormat, placeholders, and metadata.

Visual ICU Editor: Create complex plural and gender forms without memorizing ICU syntax.

CLI Integration: Automate translation sync in your CI/CD pipeline.

OTA Updates: Deploy translation fixes instantly without app store submissions.

Chrome Extension: Edit translations directly in your running app.

Setting Up IntlPull CLI

Install the CLI globally:

Terminal
npm install -g @intlpullhq/cli

Initialize in your Flutter project:

Terminal
cd my_flutter_app
intlpull init --framework flutter

The CLI auto-detects your ARB files and creates a configuration file:

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}

Push and Pull Translations

Upload your existing ARB files:

Terminal
intlpull push

Download latest translations:

Terminal
intlpull pull

Watch for changes during development:

Terminal
intlpull watch

This command syncs translations in real-time as your team makes updates in the IntlPull dashboard.

Visual Translation Editing

IntlPull's dashboard provides a rich editing experience:

  • See all languages side-by-side
  • Visual ICU editor for plurals and gender forms
  • Screenshot context for each translation
  • Comment threads for translator questions
  • Translation memory suggestions
  • AI-powered translation with context awareness

Automated String Extraction

IntlPull can scan your Dart code and extract hardcoded strings:

Terminal
intlpull scan --auto-wrap

Before:

DART
Text('Welcome to our app')

After:

DART
Text(AppLocalizations.of(context)!.welcomeToOurApp)

The CLI automatically adds the new key to your ARB files.

OTA Translation Updates

One of IntlPull's most powerful features is Over-The-Air (OTA) updates. Fix typos and update translations without going through the app store review process.

Add the OTA package:

Terminal
flutter pub add intlpull_ota

Initialize in your app:

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}

Integrate with your localization:

DART
1class MyApp extends StatelessWidget {
2  final IntlPullOTA ota;
3  const MyApp({required this.ota, super.key});
4
5  
6  Widget build(BuildContext context) {
7    return OTAProvider(
8      ota: ota,
9      child: MaterialApp(
10        localizationsDelegates: [
11          OTALocalizationsDelegate(ota),
12          AppLocalizations.delegate,
13          GlobalMaterialLocalizations.delegate,
14          GlobalWidgetsLocalizations.delegate,
15        ],
16        supportedLocales: const [
17          Locale('en'),
18          Locale('es'),
19          Locale('fr'),
20        ],
21        home: const HomeScreen(),
22      ),
23    );
24  }
25}

Now when you publish updates from the CLI:

Terminal
intlpull publish --version 1.2.0 --message "Fixed checkout button typo"

All users receive the update on their next app launch. No app store submission, no waiting for review, no forcing users to update.

CI/CD Integration

Add translation validation to your CI pipeline:

YAML
1# .github/workflows/ci.yml
2name: CI
3
4on: [push, pull_request]
5
6jobs:
7  validate-translations:
8    runs-on: ubuntu-latest
9    steps:
10      - uses: actions/checkout@v3
11      
12      - name: Setup Node
13        uses: actions/setup-node@v3
14        with:
15          node-version: '18'
16      
17      - name: Install IntlPull CLI
18        run: npm install -g @intlpullhq/cli
19      
20      - name: Validate Translations
21        env:
22          INTLPULL_API_KEY: ${{ secrets.INTLPULL_API_KEY }}
23        run: |
24          intlpull validate --missing --unused --fail-on-error
25      
26      - name: Pull Latest Translations
27        run: intlpull pull
28      
29      - name: Generate Localizations
30        run: flutter gen-l10n

This ensures:

  • No missing translations in any language
  • No unused keys cluttering your ARB files
  • Latest translations are always included in builds

Testing Your Internationalization

Proper testing ensures your app works correctly across all supported languages.

Unit Tests for Translations

Test that translations load correctly:

DART
1import 'package:flutter_test/flutter_test.dart';
2import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3
4void main() {
5  test('English translations load', () async {
6    final localizations = await AppLocalizations.delegate.load(
7      const Locale('en'),
8    );
9    
10    expect(localizations.appTitle, 'My Awesome App');
11    expect(localizations.greeting('Alice'), 'Hello, Alice!');
12  });
13
14  test('Spanish translations load', () async {
15    final localizations = await AppLocalizations.delegate.load(
16      const Locale('es'),
17    );
18    
19    expect(localizations.appTitle, 'Mi Aplicación Increíble');
20  });
21
22  test('Plural forms work correctly', () async {
23    final localizations = await AppLocalizations.delegate.load(
24      const Locale('en'),
25    );
26    
27    expect(localizations.itemCount(0), 'No items');
28    expect(localizations.itemCount(1), 'One item');
29    expect(localizations.itemCount(5), '5 items');
30  });
31}

Widget Tests

Test that your UI displays translations correctly:

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

Testing Different Locales

Force a specific locale for testing:

DART
1testWidgets('Spanish translations display correctly', (tester) async {
2  await tester.pumpWidget(
3    MaterialApp(
4      locale: const Locale('es'), // Force Spanish
5      localizationsDelegates: const [
6        AppLocalizations.delegate,
7        GlobalMaterialLocalizations.delegate,
8        GlobalWidgetsLocalizations.delegate,
9      ],
10      supportedLocales: const [Locale('en'), Locale('es')],
11      home: const HomeScreen(),
12    ),
13  );
14
15  expect(find.text('Mi Aplicación Increíble'), findsOneWidget);
16});

Golden Tests for Layout

Test that UI doesn't break with longer translations:

DART
1testWidgets('Button fits German translation', (tester) async {
2  await tester.pumpWidget(
3    MaterialApp(
4      locale: const Locale('de'),
5      localizationsDelegates: const [
6        AppLocalizations.delegate,
7        GlobalMaterialLocalizations.delegate,
8      ],
9      supportedLocales: const [Locale('de')],
10      home: Scaffold(
11        body: ElevatedButton(
12          onPressed: () {},
13          child: Text(AppLocalizations.of(context)!.submitButton),
14        ),
15      ),
16    ),
17  );
18
19  await expectLater(
20    find.byType(ElevatedButton),
21    matchesGoldenFile('golden/button_de.png'),
22  );
23});

German words tend to be longer, so this catches layout overflow issues.

Best Practices and Common Pitfalls

Do: Use Descriptive Keys

Keys should describe the content, not the location:

Bad:

JSON
1{
2  "homeScreen_title": "Welcome",
3  "screen2_button1": "Submit"
4}

Good:

JSON
1{
2  "welcomeTitle": "Welcome",
3  "submitButton": "Submit"
4}

Do: Provide Context

Always add descriptions and examples:

JSON
1{
2  "save": "Save",
3  "@save": {
4    "description": "Button text for saving user profile changes",
5    "context": "Profile edit screen"
6  }
7}

Don't: Concatenate Translations

Bad:

DART
Text(l10n.hello + ' ' + userName + '!');

Good:

JSON
{
  "greeting": "Hello, {name}!"
}
DART
Text(l10n.greeting(userName));

Different languages have different word orders, so concatenation breaks translations.

Don't: Reuse Keys for Different Contexts

Bad:

JSON
{
  "delete": "Delete"
}

Used for both "Delete Account" and "Delete Message" buttons.

Good:

JSON
1{
2  "deleteAccount": "Delete Account",
3  "deleteMessage": "Delete Message"
4}

Context matters for accurate translation.

Do: Handle RTL Languages

If supporting Arabic or Hebrew, test RTL layout:

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

Do: Cache Localizations

For performance, cache the localizations object:

DART
1class HomeScreen extends StatefulWidget {
2  const HomeScreen({super.key});
3
4  
5  State<HomeScreen> createState() => _HomeScreenState();
6}
7
8class _HomeScreenState extends State<HomeScreen> {
9  late final AppLocalizations l10n;
10
11  
12  void didChangeDependencies() {
13    super.didChangeDependencies();
14    l10n = AppLocalizations.of(context)!;
15  }
16
17  
18  Widget build(BuildContext context) {
19    return Scaffold(
20      appBar: AppBar(
21        title: Text(l10n.appTitle), // Cached lookup
22      ),
23      body: Text(l10n.welcomeMessage),
24    );
25  }
26}

Advanced Topics

Region-Specific Locales

Support regional variations like US vs UK English:

lib/l10n/
  ├── app_en_US.arb
  ├── app_en_GB.arb
  ├── app_es_ES.arb
  ├── app_es_MX.arb
DART
1supportedLocales: const [
2  Locale('en', 'US'),
3  Locale('en', 'GB'),
4  Locale('es', 'ES'),
5  Locale('es', 'MX'),
6],

Fallback Locales

Flutter automatically falls back to the base language if a regional variant isn't found:

User locale: es_AR (Spanish Argentina)
Available: en, es_ES, fr
Result: Uses es_ES (falls back to Spanish Spain)

Dynamic Locale Switching

Allow users to change language without restarting:

DART
1class MyApp extends StatefulWidget {
2  const MyApp({super.key});
3
4  
5  State<MyApp> createState() => _MyAppState();
6}
7
8class _MyAppState extends State<MyApp> {
9  Locale _locale = const Locale('en');
10
11  void _changeLocale(Locale locale) {
12    setState(() {
13      _locale = locale;
14    });
15  }
16
17  
18  Widget build(BuildContext context) {
19    return MaterialApp(
20      locale: _locale,
21      localizationsDelegates: const [
22        AppLocalizations.delegate,
23        GlobalMaterialLocalizations.delegate,
24        GlobalWidgetsLocalizations.delegate,
25      ],
26      supportedLocales: const [
27        Locale('en'),
28        Locale('es'),
29        Locale('fr'),
30      ],
31      home: HomeScreen(onLocaleChange: _changeLocale),
32    );
33  }
34}

Platform-Specific Strings

Handle iOS vs Android terminology differences:

JSON
1{
2  "tapAction": "{platform, select, ios{Tap} android{Tap} other{Click}}",
3  "@tapAction": {
4    "placeholders": {
5      "platform": {"type": "String"}
6    }
7  }
8}
DART
1import 'dart:io';
2
3String getPlatform() {
4  if (Platform.isIOS) return 'ios';
5  if (Platform.isAndroid) return 'android';
6  return 'other';
7}
8
9Text(l10n.tapAction(getPlatform()));

Production Checklist

Before launching your internationalized app:

  • All ARB files contain the same keys
  • Metadata includes descriptions for all strings
  • Plural forms tested for all languages
  • Date/time formats verified per locale
  • Number/currency formats tested
  • RTL layouts tested (if supporting Arabic/Hebrew)
  • Translation memory populated in IntlPull
  • CI/CD validates translations before merge
  • OTA updates configured for production
  • Team trained on ARB file editing workflow
  • Fallback locale configured (usually English)
  • Golden tests created for critical screens

Conclusion

Internationalizing your Flutter app with ARB files provides a robust, type-safe foundation for reaching global audiences. By following the patterns in this tutorial, you'll avoid common pitfalls and build a maintainable localization system that scales with your app.

Key takeaways:

  1. ARB files provide rich metadata and ICU MessageFormat support
  2. Flutter's code generation ensures type safety and catches errors early
  3. Proper organization and naming conventions keep translations manageable
  4. IntlPull streamlines team collaboration and deployment workflows
  5. OTA updates eliminate the app store bottleneck for translation fixes
  6. Comprehensive testing ensures quality across all supported languages

Ready to streamline your Flutter localization workflow? Try IntlPull free with 100 keys and 3 languages, or explore our Flutter documentation for advanced integration patterns. Your global users will thank you for the attention to linguistic detail, and your development team will appreciate the time saved.

Tags
flutter
arb
i18n
internationalization
localization
intl
dart
mobile
IntlPull Team
IntlPull Team
Engineering

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