← Back to Blog

Flutter Localization Not Working on iOS? The Info.plist Fix

flutterioslocalizationi18narbinfo.plist

Flutter Localization Not Working on iOS? The Info.plist Fix

Your app translates perfectly on Android. It translates in iOS debug builds too. Then you ship to TestFlight and every tester sees English — or whatever locale happens to be first in your supportedLocales. No crash, no warning, just the wrong language.

Nothing is wrong with your Dart code. The problem is that iOS never told Flutter which language the user wants — because your app never told iOS which languages it supports.

Why iOS behaves differently from Android

On Android, Flutter receives the user's system locale list directly and runs its resolution algorithm against your supportedLocales. What you declare in Dart is all that matters.

iOS adds a gatekeeper before Flutter ever runs. At launch, iOS intersects the user's preferred languages (Settings → General → Language & Region) with the localizations declared in the app bundle — the CFBundleLocalizations key in Info.plist, plus any .lproj folders. Only languages that survive that intersection are exposed to the app. If nothing matches, iOS falls back to the bundle's development region (CFBundleDevelopmentRegion), which is English in the default Flutter template.

Flutter's engine then hands that already-filtered list to PlatformDispatcher.locales, and Flutter's resolution cascade does its documented matching — exact match, then language + script, language + country, language alone — and when everything fails, it silently falls back to the first entry of supportedLocales. That final fallback is why this bug looks like "flutter ios language not changing" instead of an error: the app runs fine, in the wrong language.

A fresh Flutter project declares exactly one localization on the iOS side: English. Your twenty app_*.arb files are invisible to iOS — it doesn't read Dart or ARB, it reads bundle metadata. So a German user's preferred languages [de-DE, en-DE] get matched against [en], English wins, and Flutter never even sees de.

Why it "works" in debug but breaks in release and TestFlight

The bundle-matching rule applies to every build — debug runs just tend to mask it:

  • Your simulator or test device is usually set to English (or you force a language with MaterialApp(locale: ...) while testing), so the filtered and unfiltered results look identical.
  • Once an app declares multiple localizations, iOS remembers a per-app language choice (it appears under Settings → Your App → Preferred Language). A stale choice from an earlier install can make behavior look random between builds.
  • TestFlight is often the first time your app runs on a device whose language list contains a locale you support but never declared — exactly the case where iOS falls back to CFBundleDevelopmentRegion and Flutter falls back to English.

Long-running Flutter issues (#29414, #53119) describe this same works-in-debug pattern, and the resolution is the same every time: declare your locales in the iOS bundle.

The fix: list every locale in CFBundleLocalizations

Open ios/Runner/Info.plist and add one <string> per language your ARB files cover:

<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleLocalizations</key>
<array>
  <string>en</string>
  <string>es</string>
  <string>fr</string>
  <string>de</string>
  <string>it</string>
  <string>pt-BR</string>
  <string>ru</string>
  <string>ar</string>
  <string>ja</string>
  <string>zh-Hans</string>
</array>

Trim that array to match your actual app_*.arb files. Two rules for the codes:

  • Info.plist uses hyphens where ARB file names use underscores: app_pt_BR.arbpt-BR, app_zh_Hans.arbzh-Hans.
  • Plain languages map directly: app_de.arbde.

Then do a full rebuild (flutter build ipa). Info.plist is baked into the bundle at build time — hot reload or restart won't pick it up. Delete the previous install from the test device too, so a stale per-app language choice doesn't muddy the result.

Bonus: declared localizations are also what the App Store uses to populate the "Languages" section of your listing. The official Flutter docs recommend adding the languages in Xcode as well (Runner project → Info tab → Localizations, which creates empty .strings files) so your store page doesn't advertise English-only.

Keep the three lists from drifting apart

This bug always comes back the same way: someone adds a new app_xx.arb file, Android picks it up, and iOS quietly doesn't. Three lists must agree:

  1. Your ARB fileslib/l10n/app_en.arb, app_de.arb, …
  2. supportedLocales in your MaterialApp
  3. CFBundleLocalizations in ios/Runner/Info.plist

List 2 should never be hand-written. Use the generated list so it always mirrors your ARB files:

import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart'; // older setups: package:flutter_gen/gen_l10n/app_localizations.dart

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomeScreen(),
);

With a standard l10n.yaml (arb-dir: lib/l10n, template-arb-file: app_en.arb), flutter gen-l10n regenerates AppLocalizations.supportedLocales from whatever ARB files exist — lists 1 and 2 can't drift.

List 3 is the only manual one. A ten-second check before every release:

ls lib/l10n/app_*.arb
/usr/libexec/PlistBuddy -c "Print :CFBundleLocalizations" ios/Runner/Info.plist

If the two sets match (underscores swapped for hyphens), you're safe. Make "new ARB file → new plist entry" a line in your release checklist.

Want runtime proof of what iOS actually handed Flutter? Log the raw locale list:

MaterialApp(
  localeListResolutionCallback: (locales, supported) {
    print('Locales from OS: $locales');
    return null; // null = fall back to Flutter's default resolution
  },
  // ...
);

Run with flutter run --release on a device set to your target language. On a broken build this prints only an English locale even though the device is set to German — the smoking gun that Info.plist is the problem, not your Dart code.

Fix the plist once — then keep the ARB side honest

After CFBundleLocalizations is right, the remaining maintenance burden is the ARB files themselves. FlutterLocalisation is an ARB editor and translation-management platform for exactly this: edit your app_<locale>.arb files in a UI instead of raw JSON, manage translations across all your locales in one place, and let its ICU plural-syntax validation flag locales missing a plural category the language actually needs — a dropped many in Russian or few in Polish is the same kind of bug as this one: invisible in your test language, obvious to real users. See the features and pricing, or browse more Flutter i18n guides on the blog.

Try FlutterLocalisation free and keep every locale — Dart, ARB, and Info.plist — telling the same story.