Localize Your Flutter App Name Per Language (ARB Synced)
You ran flutter gen-l10n, translated every screen, and shipped to France, Germany and Japan. Then a user in Lyon sends a screenshot: the whole app is in French, but the icon on the home screen still says "Budget Tracker".
That is expected. gen-l10n generates Dart code. The home-screen label is read by the Android launcher and by iOS SpringBoard before any Dart runs, so it comes from native resources: strings.xml on Android and InfoPlist.strings on iOS. This post sets up both, then adds a small Dart script so your ARB files stay the single source of truth.
What onGenerateTitle does (and does not) do
Most people first try MaterialApp.onGenerateTitle:
MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
Keep it, but know its reach. On Android it sets the label shown above your app's card in the recent apps screen. On the web it sets the browser tab title. iOS ignores it and uses CFBundleDisplayName from Info.plist. On Windows, macOS and Linux the window title is set in the native runner code, so this callback does not change it there either. And on every platform, the launcher icon label is untouched.
So for flutter launcher name localization you need the native files.
Android: app_name in values-xx/strings.xml
A fresh Flutter project hardcodes the label in android/app/src/main/AndroidManifest.xml:
<application
android:label="budget_tracker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
Point it at a string resource instead:
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
Then add a default value and one folder per language under android/app/src/main/res/:
res/values/strings.xml (the fallback, used when no locale matches):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Budget Tracker</string>
</resources>
res/values-fr/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Suivi de budget</string>
</resources>
res/values-de/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Budgetplaner</string>
</resources>
The default values/ file is not optional. If a device runs a language you did not translate and there is no default, the @string/app_name lookup has nothing to fall back to.
Folder names for regions and scripts
Android folder qualifiers do not match ARB locale codes one to one:
| ARB file | Android folder | iOS folder |
|---|---|---|
app_fr.arb |
values-fr |
fr.lproj |
app_pt_BR.arb |
values-pt-rBR |
pt-BR.lproj |
app_zh_Hant.arb |
values-b+zh+Hant |
zh-Hant.lproj |
app_es_419.arb |
values-b+es+419 |
es-419.lproj |
Language plus a two-letter region uses the classic -r form. Anything with a script subtag or a numeric region needs the BCP 47 b+ form, which Android supports from 7.0 (API 24).
Also escape what Android treats as special: an apostrophe must be \' (so "L'Agenda" becomes L\'Agenda), & must be &, and a value starting with @ or ? needs a leading backslash.
iOS: CFBundleDisplayName in InfoPlist.strings
On iOS the home-screen name is CFBundleDisplayName. Flutter's template already has it in ios/Runner/Info.plist, and that value becomes the fallback. To translate it, you add a localized InfoPlist.strings file. Each language gets a copy containing one line:
/* ios/Runner/fr.lproj/InfoPlist.strings */
"CFBundleDisplayName" = "Suivi de budget";
/* ios/Runner/de.lproj/InfoPlist.strings */
"CFBundleDisplayName" = "Budgetplaner";
The Xcode step people miss
Dropping fr.lproj/InfoPlist.strings into the folder from your editor does nothing. Xcode only bundles files that are referenced in ios/Runner.xcodeproj/project.pbxproj, so a file that exists only on disk never reaches the device and the name stays in English. Do this once in Xcode:
- Open
ios/Runner.xcworkspace. - Select the Runner project (not the target), open the Info tab, and under Localizations click + for each language you ship. If Xcode asks which files to localize, click Finish. This is the same step the Flutter internationalization docs describe so the App Store lists your languages.
- Right-click the Runner folder, choose New File, pick the plain Strings File template (not String Catalog), name it
InfoPlist, and make sure the Runner target is ticked. - Select
InfoPlist.strings, open the File inspector, click Localize..., then tick every language in the list.
Xcode now creates en.lproj/InfoPlist.strings, fr.lproj/InfoPlist.strings and so on, and records them as one localized resource. From here on you can edit those files with any tool, including the script below. When you add a language later, repeat step 2 and tick InfoPlist.strings in the dialog, then rerun the script.
Two testing notes. The name follows the device language (Settings > General > Language & Region), not the language chosen inside your app. And if the label does not change, delete the app and reinstall: the home screen caches names.
One source of truth: sync from your ARB files
Translating the name in three places invites drift. Put it in the ARB files you already maintain:
{
"@@locale": "fr",
"appName": "Suivi de budget",
"@appName": {
"description": "App name shown under the home-screen icon. Keep it short."
}
}
gen-l10n turns that into AppLocalizations.of(context)!.appName for your Dart code. This script reads the same key and writes the native files. Save it as tool/sync_app_name.dart:
import 'dart:convert';
import 'dart:io';
const arbDir = 'lib/l10n';
const templateLocale = 'en';
const key = 'appName';
final arbName = RegExp(r'app_([A-Za-z0-9_]+)\.arb$');
void main() {
final files = Directory(arbDir)
.listSync()
.whereType<File>()
.where((f) => arbName.hasMatch(f.path));
for (final file in files) {
final json = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
final locale = ((json['@@locale'] as String?) ??
arbName.firstMatch(file.path)!.group(1)!)
.replaceAll('-', '_');
final name = json[key];
if (name is! String || name.isEmpty) {
stderr.writeln('skip $locale: no "$key"');
continue;
}
writeAndroid('values-${androidQualifier(locale)}', name);
if (locale == templateLocale) writeAndroid('values', name);
writeIos(locale.replaceAll('_', '-'), name);
}
}
String androidQualifier(String locale) {
final parts = locale.split('_');
if (parts.length == 1) return parts[0];
if (parts.length == 2 && RegExp(r'^[A-Za-z]{2}$').hasMatch(parts[1])) {
return '${parts[0]}-r${parts[1].toUpperCase()}';
}
return 'b+${parts.join('+')}';
}
String escapeAndroid(String s) => s
.replaceAll(r'\', r'\\')
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll("'", r"\'")
.replaceAll('"', r'\"')
.replaceFirstMapped(RegExp(r'^[@?]'), (m) => '\\${m[0]}');
void writeAndroid(String folder, String name) {
final file = File('android/app/src/main/res/$folder/app_name.xml');
file.parent.createSync(recursive: true);
file.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">${escapeAndroid(name)}</string>
</resources>
''');
print('wrote ${file.path}');
}
void writeIos(String lproj, String name) {
final file = File('ios/Runner/$lproj.lproj/InfoPlist.strings');
if (!file.existsSync()) {
stderr.writeln('warn: ${file.path} missing, localize InfoPlist.strings '
'for "$lproj" in Xcode first or it will not be bundled');
}
final value = name.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
file.parent.createSync(recursive: true);
file.writeAsStringSync('"CFBundleDisplayName" = "$value";\n');
print('wrote ${file.path}');
}
Run it from the project root:
dart run tool/sync_app_name.dart
A few design choices worth knowing:
- It writes
app_name.xmlinstead ofstrings.xml. Android merges every XML file in avaluesfolder, so a dedicated file means the script never overwrites other strings you keep instrings.xml. If you createdapp_nameby hand instrings.xmlearlier, delete that entry, or the build fails with a duplicate resource error. - It warns when an
.lprojfile did not exist yet. That is the signal that you skipped the Xcode Localizations step for that language. arbDirandtemplateLocaleshould matcharb-dirand the template file in yourl10n.yaml.
Wire it into your build so nobody forgets it, for example right after flutter gen-l10n in CI or in a pre-commit hook, then commit the generated native files.
Keep names short
Launchers truncate. Android shows roughly 10 to 12 characters under an icon depending on the launcher and font size, and iOS truncates long names with an ellipsis. German and Finnish translations are often the ones that break. The description in @appName is the place to tell translators the limit, and it travels with the string wherever the ARB goes.
Manage appName with the rest of your translations
Once appName lives in your ARB files, it is just another key. In the FlutterLocalisation ARB editor you can edit it next to every other string across all your app_<locale>.arb files instead of hand-editing JSON. The script then picks it up from the ARB files like any other key. The same editor validates ICU plurals, so a dropped few or many category for Polish or Arabic gets flagged before release. See pricing for plans, and the blog for more Flutter i18n guides.
Try FlutterLocalisation free and keep your app name, and everything else, in one place: flutterlocalisation.com.