skip to content

A Flutter grocery app localized with gen-l10n into English, German and Polish shows German to French-speaking users and English strings to some Polish users; what is wrong?

level: seniorimportance: should knowfreq 28%

answer

  1. supportedLocales order matters
  2. generated list is alphabetical
  3. preferred-supported-locales
  4. missing keys filled from template
  5. untranslated-messages-file in CI

basics

~20 s

The generated supportedLocales list is alphabetical (de, en, pl), and an unmatched device locale falls back to the first entry, German; set preferred-supported-locales: [en]. English text in Polish means keys missing from app_pl.arb were filled from the template.

solid answer

~40 s

Two separate gen-l10n behaviours. First, `AppLocalizations.supportedLocales` lists the ARB locales in alphabetical order, so it is `[de, en, pl]`; when a French-only device matches none of them, Flutter's default resolution returns the first supported locale, German. Put `preferred-supported-locales: [en]` in `l10n.yaml` so English leads the list. Second, when a key exists in `app_en.arb` but not in `app_pl.arb`, the generator fills the Polish class with the template text and only prints "pl: N untranslated message(s)"; the build still succeeds. Add `untranslated-messages-file` so the tool writes a JSON report per locale and fail CI when it is non-empty. While there, check that `MaterialApp` receives `AppLocalizations.localizationsDelegates`, not only `AppLocalizations.delegate`, or Polish and German have no Material translations.

code

yaml · 5 lines
yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
preferred-supported-locales: [en]
untranslated-messages-file: build/untranslated.json
required-resource-attributes: true

go deeper

for a junior

Remember that supportedLocales has an order and the first entry is the fallback, and that untranslated keys show template text.

for a middle

Explain that the generated supportedLocales list is alphabetical, how preferred-supported-locales reorders it, and how the untranslated report is produced.

for a senior

Diagnose wrong-language and mixed-language screens from configuration, add a CI gate on the untranslated report, and pin fallback and Material localization with widget tests.

for a principal

Decide the product's default language per market and whether releases may ship with untranslated keys, then encode that policy in configuration and CI.

## Symptom 1: French users see German **Cause.** gen-l10n builds `AppLocalizations.supportedLocales` from the ARB files it finds, sorted **alphabetically** by file: `app_de.arb`, `app_en.arb`, `app_pl.arb` give `[Locale('de'), Locale('en'), Locale('pl')]`. `MaterialApp` receives that list, and when none of the user's preferred locales matches by language or country, Flutter's default resolution returns **`supportedLocales.first`**. A device set only to French therefore gets German. **Fix.** Tell the generator which locale leads: - `preferred-supported-locales: [en]` in `l10n.yaml` moves `en` to the front of the generated list, so the fallback is English. - The listed locale must have an ARB file; otherwise generation fails with an error saying the preferred locale cannot be added. - Custom resolution logic (a `localeResolutionCallback`) is another route, but for "which language is the default" the l10n option is the smallest change and keeps the default resolution intact. ## Symptom 2: Polish users see some English strings **Cause.** Developers add keys to the template `app_en.arb` faster than translations arrive. For every key missing from `app_pl.arb`, gen-l10n **uses the template's message** in the generated Polish class and records the key as untranslated. Nothing fails: the console shows a summary such as `"pl": 3 untranslated message(s).` and suggests the report option. **Fix.** Make the gap visible and blocking: 1. Add `untranslated-messages-file: build/untranslated.json` (any path) to `l10n.yaml`. The tool writes a JSON object mapping each locale to its missing message ids, or `{}` when complete. 2. In CI, run `flutter gen-l10n` and fail the job when that file is not `{}`. 3. Optionally set `required-resource-attributes: true`, so every message must carry an `@` metadata entry, the natural home for a translator description. Regional files follow the same rule with one refinement: a key missing from `app_de_AT.arb` but present in `app_de.arb` is inherited from the German class and not reported. ## Symptom 3 (often found at the same time): Material widgets fail in German and Polish If the app wires only `AppLocalizations.delegate`, your strings are Polish but nothing supplies `MaterialLocalizations` for `pl`. The framework's built-in `DefaultMaterialLocalizations` supports only English, so in a debug build Flutter reports *"This application's locale, pl, is not supported by all of its localization delegates"*, and widgets that need localized labels (the back button tooltip, date pickers, text-selection menus) fail with *"No MaterialLocalizations found."* **Fix.** Pass `AppLocalizations.localizationsDelegates`, which already includes `GlobalMaterialLocalizations.delegate`, `GlobalCupertinoLocalizations.delegate` and `GlobalWidgetsLocalizations.delegate`. Delegate order matters only when two delegates serve the same type: the first one that supports the locale wins. Two related checks belong in the same review: - **Regional files.** If marketing later adds `app_de_AT.arb`, the generated list gains `de_AT`, and Austrian devices resolve to it; make sure it only overrides what differs and inherits the rest from `app_de.arb`. - **iOS language list.** The App Store listing reads the languages declared in the Xcode project, which gen-l10n does not edit; add German and Polish there as well. ## Diagnosing in order | Check | How | What a healthy app shows | |---|---|---| | Order of `supportedLocales` | Read the generated `app_localizations.dart` | The intended default first | | Untranslated keys | `flutter gen-l10n` output or the JSON report | No untranslated messages | | Framework delegates | `MaterialApp.localizationsDelegates` | The generated list, or a superset | | Runtime locale | `Localizations.localeOf(context)` in a debug overlay | The locale you expect per device | ## Testing it - A widget test can pump `MaterialApp` with `locale: const Locale('fr')` and the generated lists, then assert English text, which pins the fallback. - Another can pump `Locale('pl')` and a `showDatePicker` call to prove Material labels load. - Keep a CI step that fails on a non-empty untranslated report, so the next new key cannot ship half-translated. ## Why it matters for a store release Users judge a localized app by its worst screen. Falling back to the wrong language, or mixing English into Polish checkout screens, reads as a broken app; both issues are configuration, and both are cheap to lock down with the two `l10n.yaml` options and one CI check.

  • Why does a device set to French and then English get English even without preferred-supported-locales?
    Default resolution walks the user's preferred locales in order and returns the first supported match. French matches nothing, English matches `en`, so English wins. The alphabetical-first fallback only applies when no preferred locale matches at all.
  • Does a key missing from app_de_AT.arb but present in app_de.arb get reported as untranslated?
    No. The Austrian subclass extends the German class, so the message is inherited from `app_de.arb`, and the generator reports a regional gap only when the base language is missing the key too.
  • How would a widget test pin the fallback behaviour?
    Pump `MaterialApp` with `locale: const Locale('fr')`, `localizationsDelegates: AppLocalizations.localizationsDelegates` and `supportedLocales: AppLocalizations.supportedLocales`, then `expect(find.text('Your cart'), findsOneWidget)`. If the generated list order changes, the test fails.

saying these in an interview costs you the question

  • An unsupported device locale always falls back to the template language.
  • gen-l10n fails the build when a translation is missing.
  • supportedLocales order does not matter because Flutter picks the best match.
  • AppLocalizations.delegate alone also localizes Material date pickers.
  • Missing Polish keys make AppLocalizations.of(context) return null.