Skip to content

Dev Localization ARB

Florian DITTGEN edited this page May 28, 2026 · 2 revisions

Localization (ARB)

The app ships in 23 languages and auto-switches based on the device locale. Strings live in ARB fragments (lib/l10n/_fragments/*.arb) that are assembled into the per-locale top-level ARBs (lib/l10n/app_*.arb), then compiled to Dart via Flutter's gen-l10n.

Current locales

bg cs da de el en es et fi fr hr hu it lt lv nb nl pl pt ro sk sl sv

Plus the en_XA pseudo-locale — an internal text-expansion locale (auto-generated from en by widening every string ~40 %) used by widget tests to catch overflow bugs before they ship in real translations (#1699). It is regenerated by dart tool/gen_pseudo_arb.dart and never shown to end users.

English (en) is the canonical source — every key must exist there.

Build pipeline

Three commands, run in order, after any ARB-fragment edit:

dart tool/build_arb.dart       # concatenates _fragments/*.arb → app_*.arb
dart tool/gen_pseudo_arb.dart  # regenerates app_en_XA.arb from app_en.arb
flutter gen-l10n                # generates app_localizations*.dart

The first call is what you'll run most often: add the new key + its translation to every relevant fragment under lib/l10n/_fragments/, then re-run build_arb.dart to fold them into the top-level locale files. CI's codegen-drift gate fails the PR if you forget to commit the regenerated files.

How lookup works at runtime

Text(AppLocalizations.of(context)?.searchButton ?? 'Search')

Always fall back to an English literal if the locale can't resolve (defensive against ARB gaps). The static scan in test/i18n/arb_key_parity_test.dart (#729) fails CI if any locale is missing keys that exist in English.

Adding a new string

  1. Add the key to lib/l10n/app_en.arb:
    {
      "searchButton": "Search",
      "@searchButton": {
        "description": "Primary action on the search screen"
      }
    }
  2. Add translations to every other app_*.arb — at minimum de, fr, it, es, pt, da for user-facing strings.
  3. Run the generator:
    flutter gen-l10n
    # or it runs automatically on `flutter run`
  4. Use in code:
    Text(AppLocalizations.of(context)?.searchButton ?? 'Search')
  5. Add a widget test that asserts the English fallback is used when the locale is forced to something unsupported.

When you only have English

Never ship a key that only exists in English. Options:

  • Commission a translation from the community via an issue.
  • Translate yourself (even via machine translation) and mark with @@description noting it needs a native review.
  • Postpone the string — use a technical fallback that isn't user-facing.

Plurals

ICU syntax:

{
  "stationsFound": "{count, plural, =0{No stations} =1{1 station} other{{count} stations}}",
  "@stationsFound": {
    "placeholders": {
      "count": {"type": "int"}
    }
  }
}

Used as: l10n.stationsFound(results.length).

Dates, numbers, currency

Use UnitFormatter and intl — not raw Dart formatting:

UnitFormatter.formatPricePerUnit(1.659)   // "1,659 €/L" in de_DE, "1.659 €/L" in en_US
UnitFormatter.formatDistance(2.34)        // "2,3 km" in de_DE
intl.DateFormat.yMd(l10n.localeName).format(dt)

Language switching

activeLanguageProvider (lib/core/language/active_language_provider.dart) is keep-alive. Changing it rebuilds the entire widget tree — the MaterialApp is keyed on the locale code so there's no stale AppLocalizations in any subtree.

Override in tests:

await tester.pumpWidget(
  pumpApp(
    overrides: [activeLanguageProvider.overrideWithValue(const Locale('fr'))],
    child: const SearchScreen(),
  ),
);

ARB lint

The key-parity test catches missing keys. Two more rules enforced manually:

  • No HTML in ARB. Use separate keys for structural fragments; don't embed <b> or <br>.
  • Keys are camelCase, no dots, no prefixes (the ARB file is already namespaced by language).

Adding a new locale

  1. Create lib/l10n/app_ja.arb (for Japanese) with every key translated.
  2. Add to l10n.yaml's supported list (if not auto-discovered).
  3. Add const Locale('ja') to supportedLocales in app.dart.
  4. Add the language option to Settings → Language (the list auto-populates from supportedLocales).
  5. Run flutter gen-l10n to regenerate AppLocalizations.
  6. Add tests for the new locale rendering.

Parity & completeness gates

Two CI tests guard ARB hygiene:

  • test/i18n/arb_key_parity_test.dart (#729) — iterates every app_*.arb, diffs against app_en.arb, fails if any locale misses a key or has orphaned keys.
  • test/l10n/localization_completeness_test.dart — asserts that every translated locale resolves every getter at runtime (catches keys present in the ARB but missing from AppLocalizations<Locale>).

Orphans are fine during a transition (stale keys from removed features) but they should be cleaned up in a follow-up PR.

Machine translation vs native review

Machine translation is OK for a first pass on non-critical strings (help text, tooltips). Native review is required for:

  • Primary CTAs (Search, Save, Delete, Confirm)
  • Error messages (users quote these when asking for help)
  • Privacy-policy-adjacent strings
  • Legal disclaimers

When you commit machine-translated strings, mark the keys with @@description: "MT — needs native review" so reviewers can track what's provisional.

Related

Clone this wiki locally