Localization (i18n)
4 min read
Internationalisation
| Concern | Approach |
|---|---|
| Translated strings | ARB files per locale + Flutter gen-l10n |
| Plurals / genders | ICU message format ({count, plural, ...}) |
| Date / number formatting | intl package — locale-aware formatters |
| Right-to-left layout | Use directional widgets; test in TextDirection.rtl |
| Locale selection | Auto-detect via OS, allow user override |
| Translation workflow | Send ARBs to translators (Lokalise, Phrase, Crowdin) |
| Layout overflow | Pseudolocalize → 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 this | Not this |
|---|---|
EdgeInsetsDirectional.only(start: 16) | EdgeInsets.only(left: 16) |
Alignment.centerStart | Alignment.centerLeft |
BorderRadius.directional | BorderRadius.only with topLeft/topRight |
Row (auto-mirrors in RTL) | Manually swapping children |
Icon(Icons.arrow_back) ← auto-mirrors when appropriate | Hard-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
| Step | Tool |
|---|---|
Maintain app_en.arb (your source of truth) | Hand-edit; lint forbids hard-coded strings |
| Send to translators | Upload to Lokalise / Phrase / Crowdin (via CLI or CI) |
Translators provide app_es.arb, app_ja.arb, ... | Pulled back into the repo |
CI generates AppLocalizations | flutter pub get runs flutter gen-l10n |
| Test in each locale | Integration 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
-
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. -
How do you handle RTL languages in custom layouts? Use
EdgeInsetsDirectional,AlignmentDirectional,BorderRadiusDirectional— they swap left/right based onDirectionality.of(context). For custom widgets, read the directionality explicitly. Test by setting the app locale to Arabic / Hebrew and visually checking each screen. -
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. -
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.