Localize Your Flutter App Name on iOS and Android
You ran flutter gen-l10n, every Text() widget pulls from AppLocalizations, your ARB files are clean. Then you set a test device to French, and the label under the launcher icon still reads Budget Tracker.
That isn't a bug in your setup. The home-screen name is the one string gen-l10n structurally cannot produce.
Why your ARB file can never win this one
Your ARB pipeline compiles to Dart, and Dart runs inside your app's process. The launcher label is drawn before that process exists — the Android launcher reads it out of the installed APK's resource table, and iOS SpringBoard reads it out of the app bundle's Info.plist. Both resolve it against the device language, not Localizations.localeOf(context).
Three consequences worth internalizing:
- If the phone is in French but the user forced English through your in-app language picker, the icon still says the French name. Device language always wins.
- Changing it requires native resources, which means a new build and a new store release. There is no runtime fix.
MaterialApp.titleis not it either. That string feeds the Android recents / task-switcher card (and is whatonGenerateTitleexists for), not the launcher icon.
So: two native files, mirroring the locale list your lib/l10n/ folder already has.
Android: values-<locale>/strings.xml
First check the wiring in android/app/src/main/AndroidManifest.xml. Flutter's template writes a literal label, which no resource qualifier can ever override:
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
If yours still says android:label="budget_tracker", that single edit is the whole Android bug.
Now the default resource — android/app/src/main/res/values/strings.xml. This file is mandatory; it's the fallback for every locale you didn't translate, and a missing default is a crash at inflate time, not a graceful degradation:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Budget Tracker</string>
</resources>
Then one folder per locale, same file name, same string name:
<!-- android/app/src/main/res/values-fr/strings.xml -->
<resources>
<string name="app_name">Suivi Budget</string>
</resources>
<!-- android/app/src/main/res/values-ar/strings.xml -->
<resources>
<string name="app_name">متتبع الميزانية</string>
</resources>
<!-- android/app/src/main/res/values-ja/strings.xml -->
<resources>
<string name="app_name">家計簿トラッカー</string>
</resources>
Nothing extra is needed for Arabic — the label is a plain string, and RTL layout doesn't apply to a launcher caption.
The 2026 gotcha: BCP-47 qualifiers
The legacy qualifier syntax is values-<lang>-r<REGION> — values-pt-rBR, values-zh-rCN. Note the lowercase r prefix on the region; values-pt-BR silently doesn't match anything.
The legacy syntax has no slot for a script, which is a real problem for Chinese and Serbian. Android 7.0 (API level 24) introduced BCP-47 tags for resource directories, prefixed with b+ and joined with +:
res/values-b+zh+Hans/strings.xml # Simplified Chinese
res/values-b+zh+Hant/strings.xml # Traditional Chinese
res/values-b+sr+Latn/strings.xml # Serbian, Latin script
res/values-b+es+419/strings.xml # Latin American Spanish (numeric region, impossible in legacy syntax)
The catch: b+ folders are ignored below API 24. Recent Flutter releases raise the default flutter.minSdkVersion to 24, but plenty of shipping apps still pin lower in android/app/build.gradle. Check yours — if it's under 24, keep both spellings (values-zh-rCN and values-b+zh+Hans) so old devices aren't silently dropped to the English default.
While you're here: if you ship an android/app/src/main/res/xml/locales_config.xml for the Android 13+ per-app language picker, its <locale> list should be the same list. It's the same parity problem in a third place.
Android picks the new label up immediately when the user changes system language — no reinstall needed. That's a real difference from iOS, and it's why the Android side rarely gets blamed.
iOS: Runner/Resources/<lang>.lproj/InfoPlist.strings
Create this structure under ios/:
ios/Runner/
├── Info.plist
└── Resources/
├── en.lproj/InfoPlist.strings
├── fr.lproj/InfoPlist.strings
├── ar.lproj/InfoPlist.strings
└── ja.lproj/InfoPlist.strings
Each file is old-style .strings — UTF-8, "key" = "value";, and the trailing semicolon is not optional. Set both keys: CFBundleDisplayName is what SpringBoard shows, CFBundleName is the short name iOS falls back to in places like Settings.
/* ios/Runner/Resources/fr.lproj/InfoPlist.strings */
"CFBundleDisplayName" = "Suivi Budget";
"CFBundleName" = "Suivi Budget";
/* ios/Runner/Resources/ja.lproj/InfoPlist.strings */
"CFBundleDisplayName" = "家計簿トラッカー";
"CFBundleName" = "家計簿トラッカー";
Creating the files on disk is not enough. Open ios/Runner.xcworkspace, drag Resources into the project navigator, and confirm the Runner target checkbox is ticked so the .lproj folders land in Build Phases → Copy Bundle Resources. Files that exist on disk but aren't in that phase never reach the bundle.
CFBundleLocalizations: the silent killer
This is the step most write-ups skip, and its failure mode is nothing happens. Add to ios/Runner/Info.plist:
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>fr</string>
<string>ar</string>
<string>ja</string>
</array>
CFBundleLocalizations is an array of strings declaring the localizations the bundle supports. When you add languages the Xcode-native way (Project → Info → Localizations), Xcode registers them for you. When you add .lproj folders as loose resources — which is exactly what the Flutter workflow above does — nothing registers them, iOS treats the bundle as English-only, and it never opens fr.lproj. No warning, no build error, no crash. It also drives the languages shown on your App Store product page, so it's worth getting right even if the display name never changes.
The Xcode 15+ alternative: a String Catalog
If you'd rather stay in modern Xcode tooling, select Info.plist in the navigator, right-click, and choose Migrate to String Catalog. You get InfoPlist.xcstrings — a table UI where you add the key CFBundleDisplayName and fill a column per language, no .lproj folders to hand-manage. The file name is always InfoPlist.xcstrings regardless of what your plist is called. It's cleaner, but it's an Xcode-managed file that flutter create-style regeneration and some CI setups won't reproduce, so pick one approach and document it in your README.
"I did all this and it didn't work"
Usually you did, and SpringBoard is lying to you. The home-screen label is cached, so an incremental flutter run over an existing install will keep showing the old name indefinitely.
Before you debug anything: delete the app from the device and reinstall, or reboot the device. On the simulator, xcrun simctl uninstall booted <bundle-id> (or Device → Erase All Content and Settings). Push-notification banner titles are cached separately and often need the reboot specifically.
If a clean install still shows English, inspect the built bundle rather than guessing:
flutter build ios --debug --no-codesign
# Are the localizations declared?
plutil -p build/ios/iphoneos/Runner.app/Info.plist | grep -i CFBundleLocalizations -A 6
# Did the .lproj folders actually ship?
ls build/ios/iphoneos/Runner.app | grep lproj
No fr.lproj in that listing means a Copy Bundle Resources problem. An empty CFBundleLocalizations means the Info.plist edit didn't apply. Either way it's a packaging bug, not a translation bug — and you've just saved yourself an hour of re-typing French.
The locale-parity checklist
The real failure mode isn't a typo in one file — it's adding a locale in one place and forgetting the other two. Before every release, for every lib/l10n/app_<locale>.arb you ship:
-
android/app/src/main/res/values-<locale>/strings.xmlexists withapp_name -
values/strings.xml(unqualified default) exists and definesapp_name -
android:label="@string/app_name"in the manifest — not a literal -
b+folders duplicated in legacy form ifminSdkVersion < 24 -
ios/Runner/Resources/<lang>.lproj/InfoPlist.stringswithCFBundleDisplayNameandCFBundleName - the locale is listed in
CFBundleLocalizations - every
.lprojappears in Copy Bundle Resources -
locales_config.xmlmatches, if you ship one -
supportedLocalesin yourMaterialAppmatches the same list - verified on a freshly installed app, not a hot restart
Adding a locale in one place and not the others is how apps ship a half-translated identity: perfect French inside, an English icon on the home screen.
Keep the source-of-truth list honest
All of this hangs off one thing — knowing exactly which locales you actually ship. That list lives in your ARB files, and it drifts the moment someone adds app_pt.arb on a Friday.
FlutterLocalisation is an ARB editor and translation-management platform built for Flutter: edit your app_<locale>.arb files in a UI instead of raw JSON, manage translations across every locale in one place, and catch ICU plural mistakes automatically — like a dropped few/many category that Arabic, Polish or Russian genuinely need. When your locale list is visible and validated in one screen, the native parity checklist above becomes a two-minute audit instead of a bug report from a French user.
See all features or check the pricing — there's a free tier.
Try FlutterLocalisation free and get your ARB locales under control before you mirror them into native resources.