← Back to Blog

Share One AppLocalizations Across a Melos Monorepo

melosmonorepogen-l10nl10n.yamlflutter-3.32

Share One AppLocalizations Across a Melos Monorepo

You keep every ARB file in packages/shared_l10n, five white-label apps import package:flutter_gen/gen_l10n/app_localizations.dart, and one flutter upgrade later the analyzer says:

error • Target of URI doesn't exist: 'package:flutter_gen/gen_l10n/app_localizations.dart'.
error • Undefined name 'AppLocalizations'.

That import resolves to nothing because the package it pointed at never really existed. This is the fix, verified end to end on Flutter 3.47.4 stable with a two-package workspace.

Why package:flutter_gen vanished

flutter_gen was a synthetic package. The tool wrote it into your .dart_tool/package_config.json at build time, so it was never in anyone's pubspec.yaml and never on disk. In a monorepo that was always fragile: the synthetic entry only landed in the package config of the project the tool was building, so a shared package could generate into it but an app could not reliably read it back out.

Flutter stopped generating it by default in 3.32.0, and it is now gone. Setting the old flag is a hard error:

l10n.yaml: Cannot enable "synthetic-package", this feature has been removed.

Even synthetic-package: false is dead config now, and prints a warning telling you to delete the line. Generated Dart goes into a real directory in a real package, which is exactly what a monorepo needed all along. If you want the single-app version of this migration, see Fix the flutter_gen gen_l10n import error.

The layout

mono/
  pubspec.yaml            # workspace root + melos config
  apps/white_label_app/
  packages/shared_l10n/
    l10n.yaml
    lib/
      shared_l10n.dart            # export barrel
      src/l10n/arb/app_en.arb
      src/l10n/generated/         # gen-l10n writes here

1. The shared package's l10n.yaml

This lives in packages/shared_l10n/l10n.yaml, next to that package's pubspec, not at the repo root. gen-l10n resolves every path relative to the project it runs in.

arb-dir: lib/src/l10n/arb
template-arb-file: app_en.arb
output-dir: lib/src/l10n/generated
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
use-escaping: true
format: true

The two that matter most here:

  • output-dir puts the generated Dart under lib/, which is the only place another package can import from. Without it, output defaults to arb-dir, and you end up with .dart files sitting next to your .arb files. Keeping the ARBs and the output in separate folders under src/ keeps the barrel honest.
  • nullable-getter: false makes AppLocalizations.of(context) return a non-nullable instance, so consuming apps write l10n.appTitle instead of l10n!.appTitle. Shared code reads much better for it. The default is true for backwards compatibility.

There is no synthetic-package key. Delete it. The full key list is in the l10n.yaml configuration guide.

2. The pubspec line everyone deletes by mistake

Here is the trap that closed flutter#169209 as working-as-intended. People migrating off synthetic packages assume generate: true went away with them. It did not, and it is now required:

Attempted to generate localizations code without having the flutter: generate flag
turned on. Check pubspec.yaml and ensure that flutter: generate: true has been added

The flag belongs to the package that owns the l10n.yaml, so in packages/shared_l10n/pubspec.yaml:

name: shared_l10n
publish_to: none
resolution: workspace

environment:
  sdk: ^3.13.0

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any

flutter:
  generate: true

flutter_localizations and intl are real dependencies of the generated file, which imports both. Under the synthetic scheme the app carried them; now the shared package must declare them itself or it will not analyze on its own.

One thing you do not need: an assets: block. ARB files are compile-time inputs, not runtime assets. Nothing reads them from the bundle, so listing them just bloats your app.

3. The export barrel

lib/shared_l10n.dart is the whole public surface:

/// Public entry point for the shared translations.
library;

// The delegate class itself is private in the generated file; callers use the
// static `AppLocalizations.delegate` / `.localizationsDelegates` instead.
export 'src/l10n/generated/app_localizations.dart' show AppLocalizations;
export 'src/l10n/generated/app_localizations_en.dart' show AppLocalizationsEn;
export 'src/l10n/generated/app_localizations_fr.dart' show AppLocalizationsFr;

Do not try to export AppLocalizationsDelegate. gen-l10n names it _AppLocalizationsDelegate, private to the generated library, and exporting it fails with undefined_shown_name. It is reachable through the static AppLocalizations.delegate and AppLocalizations.localizationsDelegates, which is all you need.

The per-locale subclasses are optional exports, useful when a test or a golden wants to construct AppLocalizationsFr() directly without a BuildContext.

4. Consume it from every app

The app declares a normal path dependency and imports the barrel. No l10n.yaml and no generate: true required in the app, as long as the app generates nothing of its own:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  shared_l10n:
    path: ../../packages/shared_l10n
import 'package:flutter/material.dart';
import 'package:shared_l10n/shared_l10n.dart';

class WhiteLabelApp extends StatelessWidget {
  const WhiteLabelApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
      home: const HomePage(),
    );
  }
}

AppLocalizations.localizationsDelegates already bundles the Material, Cupertino and Widgets global delegates alongside your own, so that one line replaces the four-entry list you used to hand-write.

5. Per-brand strings on top of the shared set

White-label setups usually need a handful of app-only strings. Give the app its own l10n.yaml with a different output-class, then compose the delegates:

# apps/white_label_app/l10n.yaml
arb-dir: lib/l10n
template-arb-file: brand_en.arb
output-dir: lib/l10n
output-localization-file: brand_localizations.dart
output-class: BrandLocalizations
nullable-getter: false
localizationsDelegates: const [
  ...AppLocalizations.localizationsDelegates,
  BrandLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,

That app now owns an l10n.yaml, so it needs generate: true in its own pubspec too.

6. Wire it into Melos

Melos 7 and up sit on top of pub workspaces, so the root pubspec.yaml carries both:

name: mono_workspace
publish_to: none
environment:
  sdk: ^3.13.0

workspace:
  - apps/white_label_app
  - packages/shared_l10n

dev_dependencies:
  melos: ^8.9.0

melos:
  name: mono
  scripts:
    l10n:
      description: Regenerate localizations in every package that has an l10n.yaml.
      exec: flutter gen-l10n
      packageFilters:
        fileExists: l10n.yaml

Every workspace member needs resolution: workspace in its pubspec. Then melos run l10n regenerates the shared package and any app-level classes in one pass.

Do not skip this step in CI. Building the app does not regenerate the shared package's localizations: the app has no l10n.yaml, so the build hook never fires for packages/shared_l10n. I deleted the generated directory and ran flutter test in the app, and it failed at load time with an unresolved import. Either commit lib/src/l10n/generated/ or run melos run l10n before anything else in your pipeline. Committing it is the calmer choice: code review sees translation diffs, and a fresh clone analyzes without a bootstrap step.

Migration checklist

  1. Delete every synthetic-package line from every l10n.yaml.
  2. Add output-dir under lib/ in the shared package.
  3. Add generate: true plus flutter_localizations and intl to the shared package's pubspec.
  4. Write the barrel, exporting AppLocalizations only, not the delegate class.
  5. Replace package:flutter_gen/gen_l10n/... with package:shared_l10n/shared_l10n.dart across all apps.
  6. Add the Melos script, and commit or regenerate the output.

If you hit a null AppLocalizations.of(context) or a missing key partway through, the usual suspects are in Fix Flutter localization errors.

One monorepo-specific hazard

Centralizing ARBs means one dropped plural category breaks every app at once. The itemCount message above needs one and other for French, but Arabic needs zero, one, two, few, many and other, and Polish and Russian need few and many. A translator editing raw JSON will silently drop them, and gen-l10n will happily generate a class that falls back to the wrong plural form in production.

FlutterLocalisation gives you an ARB editor over your app_<locale>.arb files instead of raw JSON, manages the whole locale set in one place, and runs ICU plural-syntax validation that flags any locale missing a category its language actually needs, before it reaches your shared package.

Try FlutterLocalisation free and keep the one ARB set your whole monorepo depends on correct.