Перемикання мови без перезапуску додатку — задача, яка виглядає тривіально, поки не натрапиш на те, що половина рядків змінилася, а Fragment з ViewPager2 залишився старою мовою, тому що пережив зміну конфігурації через setRetainInstance(true). Або що DateTimeFormatter закешував Locale при першому виклику і тепер форматує дати російською, хоча користувач вибрав англійську три екрани тому. Ми, як команда з досвідом понад 5 років у мобільній розробці, стикаємося з цими проблемами регулярно. Наші інженери сертифіковані Apple та Google, і ми гарантуємо, що ваш користувач побачить актуальну мову одразу після вибору, без перезапуску додатку. Замовте аудит поточної архітектури, щоб виявити приховані проблеми локалізації.
Чому динамічне перемикання мови — складна задача?
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) } } Підхід описаний у документації Apple по Bundle
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 ключі кешу.
Як уникнути помилок при зміні мови?
Типові помилки та їх запобігання:
| Помилка | Рішення |
|---|---|
| Кешування форматувальників дат/чисел | Створювати новий екземпляр при кожній зміні мови |
| Рядки в ViewModel | Використовувати @StringRes Int або локалізувати на рівні View |
| Неоновлення відмальованих UI-елементів | Викликати notifyDataSetChanged() або використовувати реактивний підхід |
| Ігнорування системних налаштувань | Відключити автосинхронізацію, якщо мова керується додатком |
Приклад для Flutter з Riverpod
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) => LocaleNotifier()); class LocaleNotifier extends StateNotifier<Locale> { LocaleNotifier() : super(Locale('en')); void setLocale(Locale locale) { state = locale; } } Як ми реалізуємо перемикання мови?
Ми пропонуємо комплексне рішення, адаптоване під ваш стек. Процес включає:
- Аудит поточної архітектури — перевірка зберігання рядків, форматувальників, кешування
- Вибір механізму зберігання — SharedPreferences/UserDefaults для клієнтського вибору або серверний профіль для мультитенантних додатків
- Реалізація locale-провайдера з реактивним стейтом (Riverpod, Room + Flow, Combine)
- Оновлення всіх місць форматування дат, чисел, валют (
NumberFormat,DateFormat— не кешувати з locale) - Тестування на ключових екранах — deep link, push-notification landing pages, таби, діалоги
| Платформа | Підхід | Час впровадження (типовий проєкт) | Складність |
|---|---|---|---|
| iOS | Bundle swizzling або SwiftUI environment | 1-2 дні | Середня |
| Android | AppCompatDelegate.setApplicationLocales або ContextWrapper | 1-2 дні | Середня |
| Flutter | MaterialApp locale + Provider | 0.5-1 день | Низька |
| React Native | i18n library + native module (за потреби) | 1-2 дні | Середня |
Що входить у роботу
- Детальний звіт за результатами аудиту
- Реалізація механізму перемикання мови
- Налаштування рядкових ресурсів і форматувальників
- Інтеграція з існуючими сервісами (бекенд, аналітика)
- Тестування на всіх сценаріях (включаючи офлайн)
- Надання доступу до репозиторію з кодом
- Навчання команди (1 годинна сесія)
- Гарантія на впровадження 30 днів
Типові помилки та як їх уникнути
- Кешування форматувальників — завжди створюйте новий екземпляр при зміні мови
- Рядки в ViewModel — використовуйте @StringRes або локалізуйте на рівні View
- Неоновлення вже відмальованих елементів — викликайте notifyDataSetChanged() або використовуйте реактивний підхід
- Ігнорування системних налаштувань — якщо додаток керує мовою, відключіть автосинхронізацію з системою
Терміни та вартість
Терміни варіюються від 1 до 5 робочих днів залежно від складності архітектури та кількості екранів. Вартість розраховується індивідуально після аналізу кодової бази. Зв'яжіться з нами, щоб отримати консультацію та оцінку вашого проєкту. Ми проаналізуємо поточну реалізацію та запропонуємо оптимальне рішення під ключ. Отримайте консультацію з локалізації вже сьогодні.







