← Back to Blog

How to Translate a Flutter App (2026): Step by Step

fluttertranslationgen-l10narblocalizationtutorial

How to Translate a Flutter App (2026): Step by Step

You have a Flutter app in one language and you want it in five. This guide is the whole path: set up Flutter's own localization pipeline, move your strings into ARB files, get them translated (by AI, a translator or both), handle plurals and placeholders, test, and ship. Every step has code you can paste. It takes about an hour for a small app.

If you only want the short version: add flutter_localizations and intl, create l10n.yaml, put your English strings in lib/l10n/app_en.arb, run flutter gen-l10n, replace hard-coded strings with AppLocalizations.of(context)!.key, then translate the ARB file into one file per language. Everything below explains each of those steps and the traps around them.

1. Add the packages

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any

flutter:
  generate: true

intl: any avoids the classic "flutter_localizations depends on intl" version error, because flutter_localizations pins the version it needs. generate: true tells Flutter to run gen-l10n on every build.

2. Create l10n.yaml

Put this file next to pubspec.yaml:

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

nullable-getter: false gives you AppLocalizations.of(context) without the !. synthetic-package: false writes the generated file into lib/l10n so you import it by path instead of through the deprecated package:flutter_gen. If you want to tune the other options, the l10n.yaml generator builds the file for you.

3. Write the template ARB file

lib/l10n/app_en.arb is the source of truth. Every key in it becomes a typed getter or method.

{
  "@@locale": "en",
  "appTitle": "Shopping List",
  "@appTitle": { "description": "Title shown in the app bar" },

  "greeting": "Hello, {name}!",
  "@greeting": {
    "description": "Greets the signed-in user by first name",
    "placeholders": { "name": { "type": "String", "example": "Ada" } }
  },

  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "description": "How many items are on the list",
    "placeholders": { "count": { "type": "int" } }
  },

  "deleteConfirm": "Delete \"{title}\"? This cannot be undone.",
  "@deleteConfirm": {
    "placeholders": { "title": { "type": "String" } }
  }
}

Three rules that save you hours later:

  • Write descriptions. A translator (human or AI) who reads "Title shown in the app bar" translates better than one who reads only "Shopping List".
  • Never concatenate. "Hello, " + name cannot be reordered in Japanese or German. Use a placeholder.
  • Use ICU plurals from day one. English has two forms, Arabic has six, Polish has four. {count, plural, ...} lets each language declare its own.

The free ARB editor validates this file in the browser and catches a missing @ entry or a broken placeholder before the build does.

4. Generate and wire it up

flutter gen-l10n

Then register the delegates in MaterialApp:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
  home: const HomePage(),
);

AppLocalizations.localizationsDelegates already includes the Material, Cupertino and Widgets delegates, so date pickers, tooltips and the "back" button label get translated for free.

5. Replace hard-coded strings

// before
Text('Hello, $name!')
Text('$count items')

// after
Text(AppLocalizations.of(context).greeting(name))
Text(AppLocalizations.of(context).itemCount(count))

A small extension keeps call sites short:

extension L10nX on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this);
}

Text(context.l10n.greeting(name));

To find the strings you missed, grep for Text(' and Text(" in lib/. For strings outside the widget tree (a Bloc, a background push handler) see AppLocalizations without BuildContext.

6. Translate the ARB file

This is the step people underestimate. You need one file per language, with the same keys:

lib/l10n/
  app_en.arb
  app_de.arb
  app_es.arb
  app_ja.arb
  app_ar.arb

You have three realistic options.

AI translation. Fast and cheap, and good enough for UI copy when the model sees the key description and the placeholder types. The risk is plurals: a generic chat model will happily translate =1{1 item} into Polish and forget that Polish needs few and many. Use a tool that understands ICU, or review plurals by hand. FlutterLocalisation translates the whole file in one click, keeps placeholders and plural forms intact, and lets a reviewer approve each language.

A translator. Best quality, slowest, and translators do not want to edit JSON. Give them a spreadsheet or a web editor, never the raw ARB. The CSV to ARB guide covers the round trip if you go the spreadsheet route.

Both. AI first draft, human review for the languages that matter commercially. This is what most teams end up with.

Whatever you pick, the translated file must keep every placeholder exactly as written and every plural category the target language needs:

{
  "@@locale": "pl",
  "itemCount": "{count, plural, =0{Brak elementów} one{1 element} few{{count} elementy} many{{count} elementów} other{{count} elementu}}"
}

7. Catch missing translations before users do

gen-l10n can write a report of every key that is missing in some language:

# l10n.yaml
untranslated-messages-file: untranslated.json

Fail CI when that file is not empty. The CI guide for missing translations has the GitHub Actions snippet. The ARB diff tool does the same check in the browser for two files.

8. Test in another language

Force a locale in a widget test:

await tester.pumpWidget(MaterialApp(
  locale: const Locale('de'),
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomePage(),
));
expect(find.text('Einkaufsliste'), findsOneWidget);

On a device, change the system language, or add a language picker in the app and persist the choice. Changing the language at runtime covers the picker. German is roughly 30 percent longer than English, so check every button and every Row that holds text. Arabic and Hebrew flip the layout; use EdgeInsetsDirectional instead of EdgeInsets and test with Locale('ar') once.

9. Declare the languages to the stores

iOS only shows your app as localized when CFBundleLocalizations in Info.plist lists the languages, otherwise the app stays in English on an iPhone set to German. Android 13 and later need locale_config.xml for per-app language settings. Both are five-minute fixes: Info.plist for Flutter locales and Android per-app languages.

10. Keep it translated

The first translation is the easy part. Every feature after it adds keys, and the question is how a new key gets into eight languages without someone copy-pasting into eight files. The options, roughly in order of effort:

  1. A script that lists keys missing per language, run in CI (step 7).
  2. A spreadsheet round trip with a translator before each release.
  3. A localization platform that holds the keys, translates new ones with AI, lets a reviewer approve, and commits the ARB files back to the repo. That is what FlutterLocalisation does, with two-way Git sync so a key added in the dashboard shows up as a commit and a key added in the repo shows up in the dashboard.

Common errors, quickly

  • "No MaterialLocalizations found": a widget runs outside MaterialApp, usually in a test. Fix here.
  • "Target of URI doesn't exist: package:flutter_gen": you are on synthetic-package: false but still importing the old path, or the other way around. Fix here.
  • LocaleDataException with DateFormat: call initializeDateFormatting() before formatting dates in a non-English locale. Fix here.
  • App stays in English on iOS: step 9.

Try it on your own app

Create a free project on FlutterLocalisation, import app_en.arb, pick the languages, and translate them in one click. The free plan covers 500 keys. Pull the ARB files with the CLI or let the Git sync commit them, and step 6 is done.