Localization (i18n)

Medium PriorityAsked in ~45% of senior interviews

4 min read

Internationalisation

ConcernApproach
Translated stringsARB files per locale + Flutter gen-l10n
Plurals / gendersICU message format ({count, plural, ...})
Date / number formattingintl package — locale-aware formatters
Right-to-left layoutUse directional widgets; test in TextDirection.rtl
Locale selectionAuto-detect via OS, allow user override
Translation workflowSend ARBs to translators (Lokalise, Phrase, Crowdin)
Layout overflowPseudolocalize → strings grow ~30% → catch overflow early

Setup

# pubspec.yaml
dependencies:
  flutter_localizations: { sdk: flutter }
  intl: any

flutter:
  generate: true
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
// lib/l10n/app_en.arb
{
  "@@locale": "en",
  "appTitle": "My App",

  "welcomeMessage": "Welcome, {name}!",
  "@welcomeMessage": {
    "placeholders": { "name": { "type": "String" } }
  },

  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": {
    "placeholders": { "count": { "type": "int" } }
  },

  "lastLogin": "Last login: {date}",
  "@lastLogin": {
    "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" } }
  }
}
// lib/l10n/app_es.arb
{
  "@@locale": "es",
  "appTitle": "Mi Aplicación",
  "welcomeMessage": "¡Bienvenido, {name}!",
  "itemCount": "{count, plural, =0{Sin artículos} =1{1 artículo} other{{count} artículos}}",
  "lastLogin": "Último acceso: {date}"
}
// App setup
MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomeScreen(),
);

// Usage
final l = AppLocalizations.of(context)!;
Text(l.welcomeMessage('Alice'));
Text(l.itemCount(5));

flutter pub get regenerates AppLocalizations whenever ARB files change.


ICU patterns you'll use most

# Plural
"{count, plural, =0{No items} =1{1 item} other{{count} items}}"

# Gender
"{gender, select, male{He} female{She} other{They}} replied"

# Select
"{tier, select, free{Free} premium{Premium} other{Unknown}}"

# Nested
"{count, plural,
  =1{You have 1 {tier, select, free{free} premium{premium} other{}} item}
  other{You have {count} items}
}"

Why not just "You have $count items"? Because in Russian "items" has three forms based on count (1, 2-4, 5+). In Arabic, six. The ICU format lets translators express that natively per locale.


RTL — how Flutter handles it

Use thisNot this
EdgeInsetsDirectional.only(start: 16)EdgeInsets.only(left: 16)
Alignment.centerStartAlignment.centerLeft
BorderRadius.directionalBorderRadius.only with topLeft/topRight
Row (auto-mirrors in RTL)Manually swapping children
Icon(Icons.arrow_back) ← auto-mirrors when appropriateHard-coding direction-dependent icons

Test by setting MaterialApp(locale: Locale('ar')) and verify your screens mirror correctly. Manual RTL bugs are layout bugs — automated golden tests in both directions catch them.


Workflow — translation outsourcing

StepTool
Maintain app_en.arb (your source of truth)Hand-edit; lint forbids hard-coded strings
Send to translatorsUpload to Lokalise / Phrase / Crowdin (via CLI or CI)
Translators provide app_es.arb, app_ja.arb, ...Pulled back into the repo
CI generates AppLocalizationsflutter pub get runs flutter gen-l10n
Test in each localeIntegration tests + golden tests per locale

Common mistakes to avoid

❌ Hard-coded strings in screens
   Text('Welcome') scattered everywhere → not translatable.
   ✅ Always Text(l.welcomeMessage) via AppLocalizations.

❌ Concatenating strings ("Welcome, " + name + "!")
   Other languages reorder. Always use placeholders + ICU.

❌ Pluralising with if/else in Dart
   if (count == 1) "1 item" else "$count items" — works for English, breaks for Russian.
   ✅ Use ICU plural in ARB.

❌ Hard-coded "left"/"right"
   Breaks in RTL. Use start/end / EdgeInsetsDirectional.

❌ Not testing RTL
   Ship to Arabic users → app looks broken.
   ✅ Switch locale in dev; goldens in both directions.

❌ Locale override that overrides every locale
   Always supportedLocales: ALL of them, with a chosen DEFAULT.

❌ Pulling translations on every app start
   You don't need to — they're bundled with the binary.
   (Only needed if you do over-the-air translation updates — separate concern.)

❌ Date / number formatting with toString()
   '1.5' vs '1,5' depending on locale — use NumberFormat / DateFormat from intl.

Interview follow-ups

  1. Why use ICU plural instead of if (count == 1)? Because plurality rules differ by language. English has 2 forms (one, other). Russian has 3 (one, few, many). Arabic has 6. ICU's {count, plural, ...} lets each locale define its own rule structure; the runtime picks the right form based on the locale. Hand-rolled logic only works for languages whose rules match your assumption.

  2. How do you handle RTL languages in custom layouts? Use EdgeInsetsDirectional, AlignmentDirectional, BorderRadiusDirectional — they swap left/right based on Directionality.of(context). For custom widgets, read the directionality explicitly. Test by setting the app locale to Arabic / Hebrew and visually checking each screen.

  3. What's pseudolocalization and why use it? Replace each English string with an expanded, accented version ([!! Wéłcömę, Älïçé !!]) — same code points, but ~30% longer with non-ASCII characters. It catches three things: (1) hard-coded English strings (they don't transform), (2) layout overflow when strings get longer, (3) characters that break rendering. Cheap pre-translation check.

  4. How do you handle translations that arrive after a feature ships? ARB files default to the template (English) when a key is missing in another locale. Ship with English fallback for new strings; translators backfill in the next sync; pull updated ARBs and rebuild. Don't block feature launches on full translation completion.


How helpful was this content?

Please sign in to rate this article.