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— matchesflutter 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
Localeoverride in widget tests:tester.binding.window.localeTestValue = const Locale('es'). - Pseudo-localization for layout testing. Generate a
app_psaccent.arbwith stretched/accented text — surfaces overflow bugs before localized text causes them. - iOS Info.plist. Add
CFBundleLocalizationsarray 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.