Реалізація динамічного переключення мови у мобільній програмі
Переключення мови без перезапуску програми — завдання, яке виглядає тривіально, поки не натиснешся на те, що половина строк поменялась, а Fragment з ViewPager2 залишився на старій мові, тому що пережив смену конфігурації через setRetainInstance(true). Або DateTimeFormatter закешував Locale при першому виклику та тепер форматує дати на російській, хоча користувач вибрав англійську три екрани назад.
Чому це складніше, ніж кажеться
Android
Стандартний спосіб — AppCompatDelegate.setApplicationLocales(LocaleListCompat) з AndroidX (API 33+ нативно, AndroidX-бэкпорт працює до API 21). До AndroidX приходилось вручну пересоздавати Configuration та перезапускати Activity.
Проблема з setApplicationLocales: він зберігає вибір користувача у системних налаштуваннях програми. Це добре для інтеграції з системними Language Settings, але ломає сценарій, коли locale керується з бэкенда (мультитенантні програми, де мова задається профілем). Тоді потрібен власний ContextWrapper з перегрузкою attachBaseContext, який обгортає Context з потрібною Locale до того, як Activity почне інфлейтити layout.
ViewModel не пересоздається при зміні locale через setApplicationLocales — вона переживає зміну конфігурації. Строки всередину ViewModel (якщо вони туди попали — помилка архітектури) залишаться на старій мові. Строки у LiveData<String> або StateFlow<String> потрібно або зберігати як @StringRes Int та форматувати у View, або перевипускати після зміни locale.
RecyclerView.Adapter з закешованими строками потрібно явно дёрнути через notifyDataSetChanged() або краще через DiffUtil, інакше вже відрисовані елементи не перерисуються.
iOS
Bundle.main.localizedString(forKey:value:table:) читає строки з завантаженого бандла — то есть з бандла тієї локалі, що була активна при запуску. UserDefaults.standard.set(["ru"], forKey: "AppleLanguages") змінює мову лише після наступного запуску. Це обмеження iOS до версії 13.
Для iOS 13+ правильний шлях — Bundle swizzling: створюємо користувацький Bundle, який переопределяє localizedString(forKey:) та читає з бандла потрібної локалі. Типова реалізація — через розширення Bundle з збереженням поточної languageBundle у статичній змінній:
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 спрощує: environment(\.locale, Locale(identifier: "ar")) на кореневому View — та всі дочірні вью перерисуються з новою locale. Строки через LocalizedStringKey підхоплюються автоматично.
Але UIViewController-based екрани у SwiftUI-обёртці через UIViewControllerRepresentable потребують явного тригеру — NotificationCenter або @Published флаг перестройки.
Flutter
MaterialApp(locale: _currentLocale) + setState — стандартний підхід. Пакет flutter_localizations + intl для форматування. Смена locale через Provider або Riverpod (Notifier з Locale-стейтом) — UI перебудовується реактивно.
Кейс з практики: програма з cached_network_image — при зміні мови кеш зображень з alt-текстами скидався (тому що ключ кеша включав locale-залежний URL). Рішення — locale-agnostic ключі кеша.
Що робимо
- Виибираємо механізм зберігання:
SharedPreferences/UserDefaultsабо серверний профіль - Реалізуємо locale-провайдер з реактивним стейтом (Riverpod / Room + Flow / Combine)
- Аудит всіх місць форматування дат, чисел, валют —
NumberFormat,DateFormatне можна кешувати з locale - Тестуємо переключення на всіх ключових екранах включаючи deep link та push-notification landing pages
Часові рамки: 1-3 дні залежно від архітектури. Стоимість розраховується після аналізу кодової бази.







