← Back to Blog

Migrate Flutter l10n to synthetic-package: false

flutterl10ni18nmigrationflutter_gengen-l10n

Migrate Flutter l10n to synthetic-package: false

If you upgraded to Flutter 3.32 or 3.35 and your localizations suddenly stopped generating — or you're seeing warnings about package:flutter_gen — you've hit the synthetic-package removal. The fix is small and mechanical, but the failure mode is nasty: flutter gen-l10n can silently produce no files, leaving your import 'package:flutter_gen/...' lines dangling with no obvious error.

This post is the concrete before/after: the exact l10n.yaml diff, the pubspec.yaml flag you must not forget, the import rewrite, and a copy-paste codemod so a large app migrates in minutes instead of one file at a time.

What actually changed

Historically, Flutter's gen-l10n tool generated your AppLocalizations class into a synthetic package called package:flutter_gen. It never existed on disk in your lib/ — the tool injected it into .dart_tool/package_config.json at build time, and you imported it like this:

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

That synthetic package caused a long tail of problems: broken package_config.json states, tooling that couldn't resolve the import, IDEs that lost the symbols, and monorepo/workspace setups that fell over. So the Flutter team is retiring it.

The timeline, from the official breaking-change notice:

  • Landed: 3.28.0-0.0.pre
  • Stable: 3.32.0 — generating into source is the documented path
  • Removal: package:flutter_gen support goes away in a subsequent stable release

The new model is simple and honest: your localizations are generated into real files in your source tree (flutter localization generated into source), and you import them from your own package path like any other Dart file. No magic, no synthetic package, no package_config.json surgery.

The failure modes you'll actually see

Depending on your setup, upgrading surfaces one of these:

  1. Silent no-file generation. flutter gen-l10n exits 0, writes nothing, and your existing package:flutter_gen imports become unresolved. This is the confusing one — nothing errors loudly.

  2. The generate-flag error:

    Attempted to generate localizations code without having the flutter: generate flag turned on.

    This means generate: true is missing from pubspec.yaml (it's now required when generating l10n into source).

Both are fixed by the same short migration.

Step 1 — Update pubspec.yaml

The generate flag is required. If it's missing, generation is a no-op or an error. Make sure this stanza exists:

flutter:
  generate: true
  uses-material-design: true

Step 2 — The l10n.yaml diff

This is the heart of the migration. Here's the before/after.

Before (relying on the synthetic package — often just this):

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

After (synthetic-package false flutter — generate into your real source tree):

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

# The three lines that do the migration:
synthetic-package: false
output-dir: lib/l10n
nullable-getter: false

What each added line does:

  • synthetic-package: false — the switch. Stops generating package:flutter_gen and writes real .dart files instead. This is the flutter gen-l10n migrate source directory step.
  • output-dir: lib/l10n — where the generated files land. If you omit it, files go into arb-dir. Putting the generated code next to your .arb files keeps things tidy; some teams prefer lib/src/generated/l10n to signal "don't hand-edit these." Whatever you pick, it must live inside lib/ so it's importable.
  • nullable-getter: false (optional but recommended) — by default AppLocalizations.of(context) returns a nullable value (AppLocalizations?) for backwards compatibility, forcing ! at every call site. Setting it to false makes the getter non-nullable, so AppLocalizations.of(context).helloWorld just works. If you keep the default true, leave your existing ! usages alone.

Now regenerate:

flutter gen-l10n

You should see real files appear at lib/l10n/app_localizations.dart (plus app_localizations_en.dart, etc.). Commit them — they're source now, not build artifacts. (You can still let flutter run/build regenerate them; checking them in just keeps CI and fresh clones happy.)

Step 3 — Rewrite the imports (the codemod)

Every file that imported the synthetic package needs its import repointed to your new source path. The rule:

  • Old: package:flutter_gen/gen_l10n/app_localizations.dart
  • New: package:<your_package>/<output-dir minus lib/>/app_localizations.dart

If your package is myapp (the name: in pubspec.yaml) and output-dir: lib/l10n, the new import is:

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

Note that the lib/ prefix is dropped in a package: URI — lib/l10n becomes l10n.

Doing this by hand across a large app is where people burn an afternoon. Don't. Run a codemod-style find/replace across lib/ and test/:

# macOS / BSD sed (note the '' after -i)
grep -rl "package:flutter_gen/gen_l10n/" lib test \
  | xargs sed -i '' 's|package:flutter_gen/gen_l10n/|package:myapp/l10n/|g'
# Linux / GNU sed
grep -rl "package:flutter_gen/gen_l10n/" lib test \
  | xargs sed -i 's|package:flutter_gen/gen_l10n/|package:myapp/l10n/|g'

Swap myapp for your real package name and l10n for your output-dir (minus lib/). That's the whole rewrite — one command turns hundreds of imports.

Then verify nothing references the old package anymore:

grep -rn "package:flutter_gen" lib test ; echo "exit: $?"

An exit code of 1 (no matches) means you're clean.

Step 4 — Confirm and clean up

Run the analyzer and a build to catch stragglers:

flutter gen-l10n
flutter analyze
flutter run

A few gotchas to check:

  • Workspaces / melos monorepos: on 3.32+ you may need explicit-package-dependencies enabled alongside synthetic-package: false, or generate: true throws an unsupported-in-workspace error. If you're not in a Pub workspace, ignore this.
  • .gitignore: if you previously ignored generated l10n, un-ignore your new output-dir so the checked-in files persist.
  • The old flutter_gen dependency: the synthetic package:flutter_gen is unrelated to the flutter_gen/flutter_gen_runner pub packages for assets. Don't remove those unless you were actually using them for assets.

Why the source-based approach is better

Beyond just un-breaking the build, generating into source means your AppLocalizations class is a normal file: your IDE resolves it instantly, "go to definition" works, code coverage sees it, and there's no hidden package_config.json state to corrupt. It's strictly less magic.

For a full reference on every l10n.yaml key — output-class, preferred-supported-locales, use-deferred-loading, and more — see our complete l10n.yaml configuration guide.

Keep your ARB files sane while you're in here

Migrating is a good moment to check your actual translation content. A common silent bug: a locale that's missing a plural category its language requires (Arabic needs few/many, Polish and Russian have their own rules), which crashes or mis-renders at runtime. FlutterLocalisation's ARB editor lets you edit app_<locale>.arb files in a UI instead of raw JSON and runs ICU plural-syntax validation that flags exactly those dropped categories across every locale — so a regenerate-into-source migration doesn't quietly ship a broken plural.

Manage all your locales in one place, catch plural gaps before your users do, and skip the raw-JSON grind. Try FlutterLocalisation free.