In a Flutter app using gen-l10n, how do you show a translated string in a widget, and why does AppLocalizations.of(context) usually need a !?
answer
- generated class, one subclass per locale
- context must sit below MaterialApp
- Localizations.of returns a nullable
- nullable-getter defaults to true
- onGenerateTitle for the app title
basics
~20 sCall AppLocalizations.of(context) from a widget below MaterialApp and read the generated getter or method, such as cartTitle. It returns a nullable type because gen-l10n's nullable-getter option defaults to true, so call sites add ! unless that option is false.
solid answer
~40 sThe generated `AppLocalizations` class exposes one getter per plain `.arb` message and one method per message with placeholders, so a widget writes `AppLocalizations.of(context)!.cartTitle`. `of` wraps `Localizations.of<AppLocalizations>(context, AppLocalizations)`, which looks up the nearest `Localizations` scope that `MaterialApp` creates; it returns `null` when the context is above that scope, for example the context of the widget that builds `MaterialApp`. Because the gen-l10n option `nullable-getter` defaults to `true`, the static `of` is typed `AppLocalizations?` and you add `!`. Setting `nullable-getter: false` in `l10n.yaml` moves the null check inside the generated `of`, so call sites read cleanly. The lookup registers an inherited dependency, so the text rebuilds when the device language changes, and it must not run in `initState`.
code
dart · 18 linesimport 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
class CartHeader extends StatelessWidget {
const CartHeader({super.key, required this.itemCount});
final int itemCount;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListTile(
title: Text(l10n.cartTitle),
subtitle: Text(l10n.cartItems(itemCount)),
);
}
}go deeper
Know the call shape: AppLocalizations.of(context)!.someGetter from a widget below MaterialApp, and that plain messages become getters while messages with placeholders become methods.
Explain that of() wraps Localizations.of, returns null without a Localizations ancestor or loaded delegate, and that nullable-getter controls whether the null check sits in your code or in the generated class.
Show how you keep lookups out of initState and static helpers, pass the l10n instance into non-widget code, and use onGenerateTitle for the app title so locale switches rebuild everything.
Weigh a non-nullable getter against explicit null checks as a team convention, and decide how non-widget layers receive localized text without depending on BuildContext.
## What gen-l10n puts in your project When a Flutter project runs the **gen-l10n** tool (automatically on `flutter pub get` and `flutter run` once `l10n.yaml` exists, or by hand with `flutter gen-l10n`), each `.arb` message catalog becomes Dart source. For a grocery app with `app_en.arb`, `app_de.arb` and `app_pl.arb` you get: - an **abstract class `AppLocalizations`** in `app_localizations.dart`, with a static `of`, a static `delegate`, and the static lists `localizationsDelegates` and `supportedLocales`; - one **concrete subclass per locale**, such as `AppLocalizationsEn`, `AppLocalizationsDe` and `AppLocalizationsPl`, each in its own file; - a **getter** for every message without placeholders (`String get cartTitle`) and a **method** for every message with placeholders (`String cartItems(num count)`). The class name is the `output-class` option, which defaults to `AppLocalizations`. ## Reading a string in a widget 1. Import the generated file with an ordinary relative or package import, for example `import 'l10n/app_localizations.dart';`. 2. Inside `build` (or `didChangeDependencies`) of a widget that sits below `MaterialApp`, call `AppLocalizations.of(context)`. 3. Read the getter or call the method: `Text(l10n.cartTitle)`, `Text(l10n.cartItems(3))`. Most teams store the result in a local variable (`final l10n = AppLocalizations.of(context)!;`) at the top of `build`, which keeps the lines short and makes one lookup per build. ## Why the `!` is there The generated method is essentially `static AppLocalizations? of(BuildContext context) => Localizations.of<AppLocalizations>(context, AppLocalizations);`. **`Localizations.of`** walks up to the nearest `Localizations` scope and returns the resources loaded for that type, or `null` when: - there is **no `Localizations` ancestor** at all, because the context belongs to a widget above `MaterialApp` (typically the widget whose `build` creates `MaterialApp`), or - the scope exists but **no delegate for `AppLocalizations` was loaded**, because `AppLocalizations.delegate` is missing from `localizationsDelegates`. gen-l10n keeps the return type nullable because its **`nullable-getter`** option defaults to `true` (the tool's help text says this is for backwards compatibility). Put `nullable-getter: false` in `l10n.yaml` and the generated `of` performs the null check itself and returns `AppLocalizations`, so call sites drop the `!`. Either way, a lookup from the wrong place still fails at runtime; the option only moves where the null check is written. A missing translation is **not** a reason for `null`. A message absent from `app_pl.arb` is filled with the template text at generation time and reported as untranslated, so the Polish subclass still has the getter. ## Where the context must be | Where you call `AppLocalizations.of(context)` | What happens | |---|---| | `build` of a screen passed to `home` or a route | Returns the instance for the resolved locale | | `build` of the widget that constructs `MaterialApp` | Returns `null`; the `!` throws a null-check error | | `MaterialApp.onGenerateTitle` callback | Works; its context is below the app's `Localizations` | | `State.initState` | Assertion error: an inherited dependency was requested before `initState()` completed | | `didChangeDependencies` or `build` of a `State` | Works, and re-runs when the locale changes | For the app's own title, `MaterialApp.title` cannot use the generated class, so use `onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle`. ## Rebuilding when the language changes `Localizations.of` calls `dependOnInheritedWidgetOfExactType`, so every widget that reads a string is registered as a dependent of the localization scope. When the user switches the phone from English to Polish, `WidgetsApp` resolves a new locale, loads the Polish resources and every dependent rebuilds with Polish text. That is also why caching a string in a field during `initState` is wrong twice over: the lookup is not allowed there, and a cached value would not follow a language change. ## Localized text outside widgets Not every string is built inside a widget. A cart view model that composes a snackbar message, or a validator that returns an error text, has no `BuildContext` of its own. The robust pattern is to resolve `AppLocalizations` in the widget layer and hand the **instance** down: - a validator method takes an `AppLocalizations` parameter, and the form's `build` passes `AppLocalizations.of(context)!`; - a view model returns a **message id or an enum**, and the widget maps it to a localized string when it builds. Both keep non-widget code free of `BuildContext`, keep it testable with a specific locale's subclass (for example `AppLocalizationsPl()`), and make sure the text is recomputed whenever the widget rebuilds after a language change. ## Common mistakes - Importing `package:flutter_gen/gen_l10n/app_localizations.dart`: that synthetic package no longer exists; the file lives in your source tree. - Forgetting `AppLocalizations.delegate` (or the whole `AppLocalizations.localizationsDelegates` list) in `MaterialApp`, which makes every lookup return `null`. - Reading strings in a `static` helper without a `BuildContext`; pass the `AppLocalizations` instance in instead.
- How do you localize the title that MaterialApp reports to the operating system?`MaterialApp.title` is a plain `String` evaluated above the app's `Localizations`, so it cannot read `AppLocalizations`. Use `onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle`; its context includes the app's localization scope, and the callback runs again whenever the app rebuilds, for example after a locale change.
- Will a Text showing l10n.cartTitle update when the user switches the device from English to Polish?Yes. `Localizations.of` registers the widget as a dependent of the inherited localization scope. When the system locale changes, `WidgetsApp` resolves the new locale, the delegates load the Polish resources, and every dependent rebuilds with the Polish subclass.
- What changes if you set nullable-getter: false and still call AppLocalizations.of from the wrong context?The generated `of` then applies the null check itself and is typed non-nullable, so call sites have no `!`. Calling it from above `MaterialApp` still fails at runtime with a null-check error; the option changes the signature, not the lookup rules.
The Localizations scope is a hotel front desk handing out phrasebooks: only guests inside the building (widgets below MaterialApp) can ask for one, and the architect standing outside (the widget that builds MaterialApp) gets nothing back.
saying these in an interview costs you the question
- The ! is needed because some messages may be untranslated for the locale.
- AppLocalizations.of works with the context of the widget that builds MaterialApp.
- Read the strings once in initState and cache them in fields.
- Import AppLocalizations from package:flutter_gen/gen_l10n in current Flutter.
- Setting nullable-getter to false makes a lookup from any context succeed.