Localize a Flutter app: complete guide with Localingos

Flutter's i18n story is solid: ARB (Application Resource Bundle) files for source strings, flutter_localizations for the runtime, and flutter gen-l10n for typed Dart code generation. The translation pipeline is where teams stall. This guide pairs Flutter's built-in i18n with Localingos to automate the translation step while keeping the standard ARB workflow intact.

Step 1 — Install

npm install -g @localingos/cli
localingos login

Flutter ships with everything else needed in the SDK.

Step 2 — Configure pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

flutter:
  generate: true

Create l10n.yaml in project root:

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

Step 3 — Source of truth

lib/l10n/app_en.arb:

{
  "@@locale": "en",
  "welcome": "Welcome, {name}",
  "@welcome": {
    "placeholders": { "name": { "type": "String" } }
  },
  "cart": "{count, plural, =0 {Cart is empty} =1 {1 item in cart} other {{count} items in cart}}",
  "@cart": {
    "placeholders": { "count": { "type": "int", "format": "compact" } }
  }
}

ARB uses ICU MessageFormat for plurals — same syntax as iOS/Android string resources.

Step 4 — Generate Dart bindings

flutter gen-l10n

This produces lib/l10n/app_localizations.dart with a typed class. From any widget:

import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class Welcome extends StatelessWidget {
  final String userName;
  final int itemCount;

  const Welcome({super.key, required this.userName, required this.itemCount});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context);
    return Column(children: [
      Text(l10n.welcome(userName)),
      Text(l10n.cart(itemCount)),
    ]);
  }
}

Typed access — l10n.welcome(userName) is checked at compile time. No string keys to typo.

Step 5 — Configure MaterialApp

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomePage(),
    );
  }
}

Flutter automatically picks the closest supported locale based on Platform.localeName.

Step 6 — Configure Localingos

localingos.json:

{
  "projectId": "your-project-id",
  "source": {
    "path": "lib/l10n/app_en.arb",
    "locale": "en",
    "format": "arb"
  },
  "targets": {
    "path": "lib/l10n/app_{locale}.arb",
    "locales": ["es", "de", "fr", "ja", "pt-BR", "zh-CN", "ko"]
  },
  "placeholders": ["{name}", "{count}"],
  "format": "icu"
}

Three Flutter-specific things:

  • format: "arb" — Localingos preserves ARB's metadata blocks (@welcome, @cart) verbatim while translating the visible strings.
  • format: "icu" at top level — preserves ICU plural/select syntax in translations.
  • Output filename pattern uses app_{locale}.arb — matches flutter gen-l10n's convention.
localingos sync

Each target locale's ARB file appears in lib/l10n/. Re-run flutter gen-l10n to regenerate the Dart bindings, then commit both ARB files AND the generated Dart.

Step 7 — Runtime locale switching

If you want users to override their system locale in-app:

class LocaleProvider extends ChangeNotifier {
  Locale _locale = const Locale('en');
  Locale get locale => _locale;

  void setLocale(Locale newLocale) {
    if (!AppLocalizations.supportedLocales.contains(newLocale)) return;
    _locale = newLocale;
    notifyListeners();
  }
}

// In MaterialApp:
MaterialApp(
  locale: context.watch<LocaleProvider>().locale,
  // ...
)

Persist with shared_preferences if you want the choice to survive app restart.

Step 8 — RTL support

Flutter handles RTL automatically when the active locale is Arabic, Hebrew, or Persian — Directionality.of(context) returns TextDirection.rtl, and Row/Column flip accordingly. Use EdgeInsetsDirectional instead of EdgeInsets so padding flips with the layout.

Container(
  padding: const EdgeInsetsDirectional.only(start: 16, end: 8),  // flips for RTL
  child: Text(l10n.welcome(userName)),
)

Step 9 — Automate sync in CI

# .github/workflows/i18n.yml
name: i18n-sync
on:
  push: { branches: [main], paths: ['lib/l10n/app_en.arb'] }
jobs:
  sync:
    runs-on: ubuntu-latest
    permissions: { contents: write, pull-requests: write }
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { channel: stable }
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install -g @localingos/cli
      - run: localingos sync
        env: { LOCALINGOS_API_KEY: '${{ secrets.LOCALINGOS_API_KEY }}' }
      - run: flutter gen-l10n
      - uses: peter-evans/create-pull-request@v6
        with:
          branch: i18n/auto-sync
          title: 'chore(i18n): sync translations + regen Dart bindings'
          commit-message: 'chore(i18n): sync translations'

The flutter gen-l10n step regenerates Dart bindings so the new translations are immediately usable.

Production checklist

  • Test in multiple locales. Use Flutter's Locale override in widget tests: tester.binding.window.localeTestValue = const Locale('es').
  • Pseudo-localization for layout testing. Generate a app_psaccent.arb with stretched/accented text — surfaces overflow bugs before localized text causes them.
  • iOS Info.plist. Add CFBundleLocalizations array with every locale you ship — otherwise iOS won't even let users pick those languages in Settings.
  • App Store screenshots per locale. Once you have translated UI, generate localized App Store / Play Store screenshots — biggest install-rate win for international markets.

Wrap up

A Flutter app with type-safe ARB-based i18n, ICU plurals across CLDR rules, RTL support, and CI-driven translation sync. The setup uses Flutter's recommended toolchain end to end — nothing exotic.

Free tier — 5,000 words covers typical Flutter app string corpora.