Kill AppLocalizations.of(context)! with nullable-getter
If your Flutter codebase is littered with AppLocalizations.of(context)!.someKey, you already know the two things everyone hates about it: the forced-unwrap ! on every single call site, and the runtime crash it hides — Null check operator used on a null value — that shows up in production and nowhere in your tests.
There is a one-line fix. Add nullable-getter: false to your l10n.yaml, regenerate, and the ! disappears from every call site. Below is the copy-paste config, the exact find/replace to migrate an existing app, and — importantly — the honest explanation of what still throws when a locale is genuinely unsupported, so you don't trade a compile-time annoyance for a false sense of safety.
Why the ! is there in the first place
When you run flutter gen-l10n, the tool generates an AppLocalizations class with a static of method. By default (nullable-getter: true), that method returns a nullable type:
// Generated with the default nullable-getter: true
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
Because the return type is AppLocalizations?, Dart's null safety forces you to deal with the null every time you read a string:
Text(AppLocalizations.of(context)!.helloWorld) // the bang everyone copies around
That default exists purely for backwards compatibility. From the official gen-l10n option docs:
Specifies whether the localizations class getter is nullable. By default, this value is true so that
Localizations.of(context)returns a nullable value for backwards compatibility. If this value is false, then a null check is performed on the returned value ofLocalizations.of(context), removing the need for null checking in user code.
In other words, the ! doesn't go away — it just moves out of your code and into the generated file, where it belongs.
The one-line fix: nullable-getter: false
Open (or create) l10n.yaml at the root of your project and add the option:
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
Regenerate:
flutter gen-l10n
# or simply
flutter pub get
Now the generated of returns a non-nullable AppLocalizations:
// Generated with nullable-getter: false
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
And your call sites become clean:
Text(AppLocalizations.of(context).helloWorld) // no bang
This is the setting most teams should be running in 2026. The tool default stays true for legacy reasons, but there's no good argument for propagating a ! through hundreds of widgets when the framework can do the unwrap once.
Migrating an existing codebase
If you already have AppLocalizations.of(context)! scattered everywhere, the migration is mechanical.
1. Flip the flag and regenerate
Add nullable-getter: false to l10n.yaml and run flutter gen-l10n. Your app won't compile yet — every existing .of(context)! now has a redundant bang on a non-nullable value, which the analyzer flags as unnecessary_non_null_assertion. That's your to-do list.
2. Find/replace the bang
The safest pattern to strip is the exact call followed by !. A literal find-and-replace works because the token is unambiguous:
# macOS/BSD sed — replace across all Dart files
grep -rl 'AppLocalizations.of(context)!' lib \
| xargs sed -i '' 's/AppLocalizations.of(context)!/AppLocalizations.of(context)/g'
If you use a shorthand alias (e.g. context.l10n via a BuildContext extension, or a different context variable name), adjust the search string accordingly, then let dart fix clean up anything the search missed:
dart fix --apply
dart fix removes the now-unnecessary_non_null_assertion bangs automatically, so it's a good second pass to catch call sites that used a context variable other than context.
3. Sanity-check your MaterialApp delegates
This is the step people skip, and it's the one that actually matters. nullable-getter: false changes the type, not the runtime guarantee. AppLocalizations is only present in the widget tree if its delegate is registered. Confirm your MaterialApp wires up both the delegates and the supported locales:
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
The generated AppLocalizations.localizationsDelegates and AppLocalizations.supportedLocales lists are the modern, less error-prone way to do this — they include the GlobalMaterialLocalizations, GlobalWidgetsLocalizations, and GlobalCupertinoLocalizations delegates for you. If you wire delegates manually and forget AppLocalizations.delegate, you'll still crash.
What actually throws when a locale is unsupported
Here's the honest part that most posts leave out. Setting nullable-getter: false does not make crashes impossible — it relocates the !. If Localizations.of<AppLocalizations>(...) returns null, the generated ! throws the same old Null check operator used on a null value. So when does it return null?
- The delegate isn't registered. No
AppLocalizations.delegate(orAppLocalizations.localizationsDelegates) inMaterialApp→ noAppLocalizationsin the tree → crash. Fix the delegates, per the section above. - You're calling
.offrom a context above theLocalizationswidget. The classic case isMaterialApp.titleoronGenerateTitlebuilt with the same context that createdMaterialApp. UseonGenerateTitle: (context) => AppLocalizations.of(context).appTitleso you get a context below the localizations widget.
Note what is not on this list: a user's device set to a language you don't ship. Flutter's locale resolution always falls back to a supported locale (by default, the first entry in supportedLocales), so an AppLocalizations instance for some supported locale is always present. An unsupported device locale gives you fallback strings — not null, not a crash. The crash is always a wiring problem, never a "user picked Klingon" problem.
So the mental model after migrating: nullable-getter: false gives you clean, bang-free call sites, and correct localizationsDelegates gives you the actual crash-safety. You need both.
Keep the ARB files themselves clean
Removing the ! fixes the ergonomics of reading translations. The other half of a stable l10n setup is keeping the ARB files that feed gen-l10n correct — matching keys across locales, valid ICU plurals, no drift. That's exactly what the FlutterLocalisation ARB editor is built for: edit app_<locale>.arb files in a UI instead of hand-editing raw JSON, manage translations across every locale in one place, and catch missing ICU plural categories (like a dropped few/many for Arabic, Polish, or Russian) before they ship as runtime surprises.
For the surrounding config, our complete l10n.yaml configuration guide walks through every option, and the localization errors fixes guide covers the other common AppLocalizations null causes.
TL;DR
- Add
nullable-getter: falsetol10n.yaml, runflutter gen-l10n. - Find/replace
AppLocalizations.of(context)!→AppLocalizations.of(context), thendart fix --apply. - Verify
MaterialAppusesAppLocalizations.localizationsDelegatesandsupportedLocales. - Remember: the crash is a delegate/context wiring bug, not a missing-translation bug.
Cleaner call sites, one honest place where the unwrap happens, and a clear model of what can still go wrong.
Try FlutterLocalisation free and manage your ARB files — plurals, missing keys, and all — without touching raw JSON.