Localize Dart Enum Labels in Flutter Dropdowns
You have a settings screen with a DropdownButton backed by an enum — PaymentType, OrderStatus, Category. It works in English. Then you add flutter_localizations, wire up your .arb files, and try to translate the option labels. That's where it falls apart:
enum PaymentType { creditCard, payPal, bankTransfer, applePay }
// ❌ This does NOT compile.
enum PaymentType {
creditCard,
payPal,
bankTransfer,
applePay;
String get label => AppLocalizations.of(context)!.creditCard; // no context here!
}
Enum members are compile-time constants. They exist before any BuildContext does, so they can't call AppLocalizations.of(context) — there is no context to reach a Localizations widget with. A translated string depends on the current locale at runtime; a const cannot.
The usual "fix" is a sprawling switch copy-pasted into every screen that renders the dropdown. Below is a cleaner pattern: the localization lives in one extension method, dropdown items are built by a single generic helper, and the Dart compiler (plus one runtime assert) guarantees you never ship a missing label.
Why enums can't hold translations
The generated AppLocalizations class turns each ARB key into an instance getter, resolved per locale via the widget tree. If you're new to the setup, our Flutter localization complete guide walks through l10n.yaml, flutter gen-l10n, and the ARB format.
The important constraint for enums: Flutter has no runtime reflection in release builds. You cannot do appLocalizations['paymentType_creditCard'] or dynamically invoke a getter by string. So a naming convention alone can't wire enum → translation automatically; you need an explicit mapping somewhere. The goal is to write that mapping exactly once and let the tooling catch mistakes.
Step 1: an ARB-key convention
Pick a predictable key scheme — <enumName>_<valueName> in lowerCamelCase — so keys are greppable and reviewable. In lib/l10n/app_en.arb:
{
"paymentType_creditCard": "Credit card",
"paymentType_payPal": "PayPal",
"paymentType_bankTransfer": "Bank transfer",
"paymentType_applePay": "Apple Pay"
}
And app_fr.arb:
{
"paymentType_creditCard": "Carte bancaire",
"paymentType_payPal": "PayPal",
"paymentType_bankTransfer": "Virement bancaire",
"paymentType_applePay": "Apple Pay"
}
Run flutter gen-l10n (or let the build do it) and you get getters like l10n.paymentType_creditCard.
Step 2: one extension method per enum
Put the mapping on the enum itself using a Dart 3 switch expression. This is the key trick: a switch expression over an enum is exhaustive — if you add PaymentType.giftCard later and forget to map it, the code won't compile. No runtime surprise, no forgotten case.
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension PaymentTypeL10n on PaymentType {
String label(AppLocalizations l10n) => switch (this) {
PaymentType.creditCard => l10n.paymentType_creditCard,
PaymentType.payPal => l10n.paymentType_payPal,
PaymentType.bankTransfer => l10n.paymentType_bankTransfer,
PaymentType.applePay => l10n.paymentType_applePay,
};
}
This is a switch — but a single, exhaustive one that lives next to the enum, not scattered across screens. You take AppLocalizations as a parameter instead of a BuildContext, so the method stays pure and testable.
Step 3: a generic dropdown builder
Now the part that removes the per-screen boilerplate. Write one helper that turns any enum plus a label function into DropdownMenuItems:
List<DropdownMenuItem<T>> localizedDropdownItems<T extends Enum>(
Iterable<T> values,
String Function(T value) label,
) =>
values
.map((v) => DropdownMenuItem<T>(value: v, child: Text(label(v))))
.toList();
Every enum-backed dropdown in the app now looks the same:
DropdownButton<PaymentType>(
value: selected,
items: localizedDropdownItems(
PaymentType.values,
(v) => v.label(context.l10n),
),
onChanged: (v) => setState(() => selected = v),
)
Here context.l10n is the well-known BuildContext extension:
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
Add a new dropdown for OrderStatus? Write one extension, reuse the same localizedDropdownItems helper. No new switch per screen.
The runtime assert for a map-based variant
The switch expression gives you compile-time safety, which is the strongest guarantee. But some teams prefer a Map — for example when the same labels feed chips, filters, and dropdowns, or when labels come from a lookup table. A map isn't checked for completeness by the compiler, so add a debug assert that fails loudly the moment a value is missing:
extension OrderStatusL10n on OrderStatus {
static Map<OrderStatus, String Function(AppLocalizations)> _labels(
AppLocalizations _,
) =>
{
OrderStatus.pending: (l) => l.orderStatus_pending,
OrderStatus.shipped: (l) => l.orderStatus_shipped,
OrderStatus.delivered: (l) => l.orderStatus_delivered,
};
String label(AppLocalizations l10n) {
final map = _labels(l10n);
assert(
map.length == OrderStatus.values.length,
'Missing localized labels for: '
'${OrderStatus.values.where((v) => !map.containsKey(v)).toList()}',
);
return map[this]!(l10n);
}
}
assert runs only in debug and profile builds, so it costs nothing in release. During development it names the exact enum values you forgot to translate. Prefer the switch expression when you can — it turns that runtime assert into a compile error — and keep the map only when a map's flexibility is genuinely worth losing that guarantee.
Keeping ARB keys and enums in sync
The one remaining risk is a typo or drift between your enum, the extension, and the ARB files. Two habits keep it honest:
- A tiny golden test that iterates
PaymentType.valuesand asserts every.label()returns a non-empty string for each supported locale. It fails the instant a key is renamed on one side only. - A single source of truth for your keys. Editing raw JSON across
app_en.arb,app_fr.arb, and a dozen more by hand is where mismatches creep in. FlutterLocalisation's ARB editor shows every key across every locale side by side, flags missing translations, and exports clean ARB thatflutter gen-l10nconsumes directly — so yourenumName_valueNamekeys stay consistent everywhere.
Wrapping up
Enums can't reach AppLocalizations.of(context) because they're compile-time constants and Flutter has no runtime reflection. Instead of a giant switch per screen, centralize the mapping in one exhaustive extension method, build items with a single generic helper, and lean on the compiler (or a debug assert) to catch missing labels. You get type-safe, fully localized dropdowns and one obvious place to update when a value is added.
Managing the .arb keys behind all of this is the part that scales badly by hand. Try FlutterLocalisation free to edit, translate, and validate your ARB files in one place — and keep every enum label in sync across all your locales.