Реалізація динамічного перемикання мови в мобільному додатку

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1734 послуг
Реалізація динамічного перемикання мови в мобільному додатку
Середній
від 1 дня до 3 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    562

Перемикання мови без перезапуску додатку — задача, яка виглядає тривіально, поки не натрапиш на те, що половина рядків змінилася, а 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;
  }
}

Як ми реалізуємо перемикання мови?

Ми пропонуємо комплексне рішення, адаптоване під ваш стек. Процес включає:

  1. Аудит поточної архітектури — перевірка зберігання рядків, форматувальників, кешування
  2. Вибір механізму зберігання — SharedPreferences/UserDefaults для клієнтського вибору або серверний профіль для мультитенантних додатків
  3. Реалізація locale-провайдера з реактивним стейтом (Riverpod, Room + Flow, Combine)
  4. Оновлення всіх місць форматування дат, чисел, валют (NumberFormat, DateFormat — не кешувати з locale)
  5. Тестування на ключових екранах — 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 робочих днів залежно від складності архітектури та кількості екранів. Вартість розраховується індивідуально після аналізу кодової бази. Зв'яжіться з нами, щоб отримати консультацію та оцінку вашого проєкту. Ми проаналізуємо поточну реалізацію та запропонуємо оптимальне рішення під ключ. Отримайте консультацію з локалізації вже сьогодні.

Локалізація мобільних додатків: i18n, RTL, динамічне перемикання мови

При локалізації додатків ми часто стикаємося з неочевидними помилками: дата 04/05 для американця — це 5 квітня, для європейця — 4 травня. Сума «1,000.50» в більшості країн — тисяча з половиною, в Німеччині — один і п'ятдесят центів. Число слів для «1 файл», «3 файли», «5 файлів» в українській — три різні форми; арабська має шість форм множини. Якщо архітектура додатку не враховує це з самого початку, локалізація перетворюється на серію патчів. Наш 7-річний досвід у мобільній розробці та 50+ релізів в App Store і Google Play підтверджують: правильний i18n/l10n з першого коміту окупається багаторазово.

Як налаштувати базову інфраструктуру локалізації?

iOS: від Localizable.strings до String Catalogs

Історично рядки в iOS зберігалися в Localizable.strings — простий key=value формат. З Xcode 15 з'явилися String Catalogs (.xcstrings) — JSON-based формат, який зберігає всі локалі в одному файлі, відображає статус перекладу (перекладено/застаріло/відсутній) та інтегрований з Xcode UI.

String(localized: "welcome_title") в Swift 5.7+ замінює NSLocalizedString(...). Коротше, типобезпечніше. String Interpolation в локалізованих рядках: String(localized: "items_count \(count)") з правилом плюралізації в .xcstrings — система автоматично вибере потрібну форму для мови. Плюралізація через .stringsdict (старий підхід) або прямо в String Catalog з NSStringPluralRuleType. Для української потрібно визначити форми one (1 файл), few (3 файли), many (5 файлів), other (fallback). Пропустити few для української — означає отримати «5 файлів» там де має бути «3 файли». String Catalogs скорочують час налаштування вдвічі порівняно з .stringsdict.

Android: XML resources та рядкові формати

res/values/strings.xml для базової локалі (en), res/values-uk/strings.xml для української. Plural strings через <plurals> з <item quantity="one">, <item quantity="few">, <item quantity="many">. resources.getQuantityString(R.plurals.file_count, count, count) — перший count вибирає форму, другий підставляється в рядок.

В Compose: stringResource(R.string.key) та pluralStringResource(R.plurals.file_count, count, count). Типобезпечна альтернатива — бібліотека Lyricist, яка генерує типізовані рядки з анотацій.

Android App Bundle з android:splitByLocale="true" в bundle.gradle — ресурси доставляються тільки для мов пристрою. APK зменшується на 15-20%, ресурси потрібних локалей довантажуються через Play Asset Delivery. Важливо: на Android 8+ Configuration.locales — список, не одна мова.

Flutter: intl та шари абстракції

Flutter intl пакет — стандарт. AppLocalizations.of(context).welcomeTitle генерується з ARB-файлів (app_en.arb, app_uk.arb). flutter gen-l10n генерує типізований код. Pluralization через {count, plural, one{# файл} few{# файли} many{# файлів} other{# файла}} в ARB.

Для великих додатків з 50+ мовами — easy_localization з підтримкою YAML/JSON/CSV форматів та lazy loading перекладів: не всі 50 мов завантажуються одразу, тільки потрібна. Це знижує розмір початкового завантаження на 30%.

Порівняльна таблиця підходів

Параметр iOS (String Catalogs) Android (XML) Flutter (ARB)
Формат зберігання JSON (.xcstrings) XML JSON (ARB)
Плюралізація Вбудована в Xcode <plurals> ICU message syntax
Типобезпека String Catalog – кодогенерація (Swift 5.9) R.java / ViewBinding gen-l10n
Статус перекладів Візуальний в Xcode Тільки сторонні інструменти Тільки сторонні інструменти
RTL з коробки Auto Layout (leading/trailing) supportsRtl + start/end Directionality Widget

Як реалізувати RTL-підтримку без переписування UI?

Арабська, іврит, перська, урду — мови RTL (Right-to-Left). Це змінює не тільки напрямок тексту, а й всю розкладку UI: back button справа, іконки дзеркально, паддінги та марджини інвертуються.

На iOS все робиться через semanticContentAttribute та Auto Layout. Layout constraint-и з leading/trailing (не left/right) автоматично інвертуються при RTL. UIView.semanticContentAttribute = .forceRightToLeft для конкретного компонента. Системні компоненти (UINavigationController, UITableView, UIStackView) перемикаються автоматично при RTL-локалі. Проблеми виникають з кастомними UI, де розробник жорстко використав left/right констрейти або frame-based layout. Ми в таких випадках переписуємо кастомні в'ю на Auto Layout — це займає 1-2 дні на екран.

На Android android:supportsRtl="true" в AndroidManifest включає RTL-підтримку. start/end замість left/right в XML-атрибутах: paddingStart, layout_marginEnd, textAlignment="viewStart". LayoutInflater з android:layoutDirection="rtl" для прев'ю. Іконки з напрямленістю (стрілки, шеврон) потрібно дзеркалювати — android:autoMirrored="true" в drawable для автоматичного інвертування при RTL.

На Flutter Directionality віджет з TextDirection.rtl керує напрямком для піддерева. Padding(EdgeInsetsDirectional.fromSTEB(...)) замість EdgeInsets.only(left:...). Row автоматично враховує TextDirection з Directionality. Більшість Material віджетів RTL-ready, але кастомні CustomPainter — ні: потрібно отримувати TextDirection з context та враховувати вручну.

Тестування RTL: на iOS Settings → General → Language & Region → Region: Saudi Arabia перемикає в RTL режим без зміни мови системи. На Android adb shell setprop debug.force.rtl 1 форсує RTL для налагодження. Це дозволяє виявити до 80% проблем RTL до релізу.

Чому динамічне перемикання мови — вузьке місце?

Перемикання мови без перезапуску додатку — нетривіальне завдання, особливо якщо система побудована на системній локалі. Ми виділили три основні підходи.

iOS не підтримує зміну мови додатку без перезапуску нативно. Найчистіший підхід — зберігати вибрану мову в UserDefaults, при запуску створювати Bundle з потрібною локалізацією, використовувати кастомний NSLocalizedString через цей Bundle. Bundle.setLanguage("uk") через swizzling Bundle.localizedString(forKey:value:table:) — працює, але це runtime swizzling, що не ідеально. Альтернатива: власна система рядків поверх NSBundle, яка перечитує файли при зміні мови. При перемиканні — перестворити кореневий ViewController.

Android з API 33: LocaleManager.setApplicationLocales() — офіційний API для зміни мови додатку без перезапуску системи, без рекреації Activity якщо використовувати AppCompatDelegate.setApplicationLocales(). До API 33 — Configuration.setLocale() + recreate() для Activity. При зміні мови потрібно повідомити всі відкриті Activity через broadcast або ViewModel. Для Android 12+ ми також використовуємо android:localeConfig в маніфесті — це дозволяє системі дізнатися підтримувані мови без додаткової конфігурації.

Flutter — найпростіший з трьох. LocalizationsDelegate перезавантажується при зміні locale в MaterialApp. Зберігаємо вибрану мову в провайдері (Riverpod/Provider/Bloc), зміна locale в MaterialApp перебудовує дерево з новими рядками. Практично без бойлерплейту при використанні easy_localization. Однак є нюанс: всі StatefulWidget, які не підписані на зміну локалі, не оновляться — потрібно явно передавати локаль через InheritedWidget або перестворювати дерево.

Форматування дат, чисел, валют

DateFormatter (iOS) та DateFormat (Android, intl) — завжди з явним locale, ніколи без нього. DateFormatter().dateStyle = .medium з locale = Locale(identifier: "uk_UA") дасть «4 травня», з Locale(identifier: "en_US") — «May 4». Ми використовуємо RelativeDateTimeFormatter (iOS 13+) та RelativeTimeFormatter через intl пакет — не винаходьте велосипед з ручним форматуванням.

NumberFormatter / NumberFormat.currency() для валют. Символ валюти, роздільники тисяч та дробової частини — все locale-специфічно. Хардкодити «₴» або «.» як роздільник — помилка. Locale(identifier: "uk_UA") + NumberFormatter.numberStyle = .currency з currencyCode = "UAH" дасть правильне форматування автоматично.

Типові помилки при локалізації

Конкатенація рядків замість форматування: "Hello, " + name + "!" працює для SVO-мов, але в японській ім'я йде перед зверненням. String(format: "greeting %@", name) з greeting = "%@ さん、こんにちは" в японському файлі — правильно. Фіксований розмір UI під текст: німецька в середньому на 30% довша за англійську. AutoLayout з правильними констрейнтами, adjustsFontSizeToFitWidth там де допустимо, динамічна зміна висоти комірок через UITableView.automaticDimension. Зображення з вбудованим текстом вимагають локалізованих версій або заміни на text overlay.

Процес роботи: як ми локалізуємо додаток під ключ

  1. Аналітика та аудит (2-5 днів) — рев'ю коду на i18n-готовність, виявлення хардкоду, оцінка RTL-складності, підготовка карти екранів.
  2. Проектування архітектури (3-7 днів) — вибір стеку (String Catalogs/ARB/XML), налаштування пайплайнів автоматизації (Crowdin/Lokalise), створення базових рядків.
  3. Реалізація (1-4 тижні залежно від обсягу) — впровадження i18n, плюралізації, RTL, форматування. Паралельно — переклад контенту.
  4. Тестування (3-7 днів) — функціональне (зміна мови, RTL, відображення чисел), лінгвістичне (LQA), скріншотне тестування (локалізовані стор-скріншоти).
  5. Деплой та моніторинг (1-2 дні) — публікація в сторах, налаштування аналітики (події на мови), зворотній зв'язок від користувачів.

Що входить в deliverables

  • Вихідний код з повною i18n-інфраструктурою
  • Документація з додавання нової мови (playbook)
  • Файли перекладів (ARB/XML/xcstrings) + глосарій
  • Автоматизація: CI/CD інтеграція з сервісами перекладів
  • Доступ до репозиторію з повною історією змін
  • Навчання команди замовника (1-2 сесії)
  • Гарантія на код — 6 місяців, безкоштовні фікси багів локалізації

Строки та вартість

Етап Строк (орієнтовно) Що входить
Додавання однієї нової мови (без RTL) 2-3 дні технічної роботи + час перекладу Налаштування рядків, тестування, стор-скріншоти
Первинна локалізація з нуля (10-15 екранів, без RTL) 2-3 тижні Архітектура, переклад, тестування
Проект з RTL-підтримкою та динамічним перемиканням 4-6 тижнів Все включено + адаптація UI, перезбирання кастомних компонентів

Вартість розраховується індивідуально на основі обсягу коду, кількості мов та складності RTL. В середньому економія бюджету за рахунок автоматизації досягає 40%. Ми оцінюємо проект безкоштовно — зв'яжіться, щоб отримати кошторис та план робіт.

Чому варто обрати нас

Ми сертифіковані розробники (Apple WWDC Scholarship, Google Associate Android Developer). За 5 років на ринку реалізували понад 50 проектів з локалізацією, включаючи додатки для 15 мов з RTL. Гарантуємо дотримання App Store Review Guidelines (Section 4.2/5.1) та Google Play Policies (User Data). Наші клієнти відзначають скорочення часу виходу на нові ринки в середньому на 40% за рахунок автоматизації перекладів та продуманої архітектури.

Додаткову інформацію про інтернаціоналізацію та локалізацію можна знайти в Wikipedia та специфікації RTL.

Отримайте консультацію по вашому проекту — залиште заявку на сайті, ми підготуємо детальний план та строки. Замовте аудит локалізації вашого додатку: перший етап показує до 80% вузьких місць без витрат на реалізацію.