Implementing Dynamic Language Switching in Mobile Applications
Language switching without app restart — task looks trivial until hitting that half strings changed, but Fragment with ViewPager2 remained on old language because survived config change through setRetainInstance(true). Or DateTimeFormatter cached Locale on first call and now formats dates in Russian, though user selected English three screens ago.
Why This Is More Complex Than Seems
Android
Standard way — AppCompatDelegate.setApplicationLocales(LocaleListCompat) from AndroidX (API 33+ natively, AndroidX backport works to API 21). Before AndroidX had to manually recreate Configuration and restart Activity.
Problem with setApplicationLocales: saves user choice in system app settings. Good for integration with system Language Settings, breaks scenario when locale managed from backend (multi-tenant apps where language set by profile). Then need custom ContextWrapper overriding attachBaseContext, wrapping Context with needed Locale before Activity starts inflating layout.
ViewModel doesn't recreate on locale change through setApplicationLocales — survives config change. Strings inside ViewModel (if got there — architecture error) remain on old language. Strings in LiveData<String> or StateFlow<String> either store as @StringRes Int and format in View, or re-emit after locale change.
RecyclerView.Adapter with cached strings needs explicitly pull through notifyDataSetChanged() or better through DiffUtil, otherwise already rendered items won't re-render.
iOS
Bundle.main.localizedString(forKey:value:table:) reads strings from loaded bundle — bundle of locale active at startup. UserDefaults.standard.set(["ru"], forKey: "AppleLanguages") changes language only after next launch. This iOS limitation before version 13.
For iOS 13+ correct way — Bundle swizzling: create custom Bundle overriding localizedString(forKey:) reading from needed locale bundle. Typical implementation — through Bundle extension storing current languageBundle in static variable:
private var bundleKey: UInt8 = 0
class LanguageBundle: Bundle {
override func localizedString(forKey key: String,
value: String?,
table tableName: String?) -> String {
guard let bundle = objc_getAssociatedObject(self, &bundleKey) as? Bundle else {
return super.localizedString(forKey: key, value: value, table: tableName)
}
return bundle.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static func setLanguage(_ language: String) {
object_setClass(Bundle.main, LanguageBundle.self)
let path = Bundle.main.path(forResource: language, ofType: "lproj")
let bundle = path.flatMap { Bundle(path: $0) } ?? Bundle.main
objc_setAssociatedObject(Bundle.main, &bundleKey, bundle, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
NotificationCenter.default.post(name: .languageDidChange, object: nil)
}
}
SwiftUI simplifies: environment(\.locale, Locale(identifier: "ar")) on root View — all child views re-render with new locale. Strings through LocalizedStringKey picked automatically.
But UIViewController-based screens in SwiftUI-wrapper through UIViewControllerRepresentable need explicit trigger — NotificationCenter or @Published rebuild flag.
Flutter
MaterialApp(locale: _currentLocale) + setState — standard approach. Package flutter_localizations + intl for formatting. Locale change through Provider or Riverpod (Notifier with Locale-state) — UI re-builds reactively.
Case from practice: app with cached_network_image — on language change cache with alt-text images cleared (because cache key included locale-dependent URL). Solution — locale-agnostic cache keys.
What We Do
- Choose storage mechanism:
SharedPreferences/UserDefaultsor server profile - Implement locale-provider with reactive state (Riverpod / Room + Flow / Combine)
- Audit all date, number, currency formatting places —
NumberFormat,DateFormatcan't cache with locale - Test switching on all key screens including deep link and push-notification landing pages
Timeframe: 1-3 days depending on architecture. Cost calculated after codebase analysis.







