Локалізація мобільних додатків: i18n, RTL, динамічне переключення мови
Локалізація—не просто переклад строк. Дата «04/05/2024» у США означає 5 квітня, в Європі—4 травня. Сума «1,000.50» в більшості країн—тисяча з половиною, в Германії—один та п'ятдесят центів. Кількість слів для «1 файл», «3 файли», «5 файлів» в українській—три різні форми; арабська має шість форм множинного числа. Якщо архітектура додатку не враховує це з самого початку, локалізація перетворюється на серію патчів.
Базова інфраструктура: строки, формати, плюралізація
iOS: Localizable.strings та String Catalogs
Історично строки в iOS зберігалися в Localizable.strings—простому форматі key=value. З Xcode 15 з'явилися String Catalogs (.xcstrings)—JSON-based формат, який зберігає всі локалі в одному файлі, показує статус перекладу (перекладено/застарілі/відсутні) та інтегрований з UI Xcode.
String(localized: "welcome_title") в Swift 5.7+ замість NSLocalizedString("welcome_title", comment: ""). Коротше, типобезпечніше. String Interpolation в локалізованих строках: String(localized: "items_count \(count)") з правилом плюралізації в .xcstrings—система автоматично вибирає правильну форму для мови.
Плюралізація через .stringsdict (старий підхід) або прямо в String Catalog з NSStringPluralRuleType. Для української потрібно визначити форми one (1 файл), few (3 файли), many (5 файлів), other (fallback). Пропустити few для української—означає отримати «5 файлів» там де повинно бути «3 файли».
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 зменшується, ресурси потрібних локалей завантажуються через 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 генерує типизований код. Плюралізація через {count, plural, one{# файл} few{# файли} many{# файлів} other{# файлів}} в ARB.
Для великих додатків з 50+ мовами—easy_localization з підтримкою YAML/JSON/CSV форматів та lazy loading перекладів: не все 50 мов завантажуються відразу, тільки потрібна.
RTL: письмо справа наліво
Арабська, іврит, перська, урду—RTL (Right-to-Left) мови. Це змінює не тільки напрямок тексту, але всю раскладку UI: кнопка back справа, іконки дзеркально, паддинги та марджини інвертовані.
На iOS все робиться через semanticContentAttribute та Auto Layout. Layout constraint-и з leading/trailing (не left/right) автоматично інвертуються при RTL. UIView.semanticContentAttribute = .forceRightToLeft для конкретного компонента. Системні компоненти (UINavigationController, UITableView, UIStackView) переключаються автоматично при RTL-локалі. Проблеми виникають з кастомними UI, де розробник жорстко використав left/right констрейнти або frame-based layout.
На Android android:supportsRtl="true" в AndroidManifest включає RTL-підтримку. start/end замість left/right в XML-атрибутах: paddingStart, layout_marginEnd, textAlignment="viewStart". LayoutInflater з android:layoutDirection="rtl" для preview. Іконки з напрямком (стрілки, шевроны) потрібно дзеркалювати—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 для debugging.
Динамічне переключення мови
Переключення мови без перезапуску додатку—нетривіальне завдання, особливо якщо система побудована на системній локалі.
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.
Flutter—найпростіший з трьох. LocalizationsDelegate перезагружається при зміні locale в MaterialApp. Зберігаємо вибрану мову в провайдері (Riverpod/Provider/Bloc), зміна locale в MaterialApp перестраює дерево з новими строками. Практично без бойлерплейту при використанні easy_localization.
Форматування дат, чисел, валют
DateFormatter (iOS) та DateFormat (Android, intl)—завжди з явним locale, ніколи без нього. DateFormatter().dateStyle = .medium з locale = Locale(identifier: "uk_UA") даст «4 травня 2024 р.», з Locale(identifier: "en_US")—«May 4, 2024».
NumberFormatter / NumberFormat.currency() для валют. Символ валюти, розділювачи тисяч та дробної частини—все locale-специфічно. Хардкодити «₴» або «,» як розділювач—помилка. Locale(identifier: "uk_UA") + NumberFormatter.numberStyle = .currency з currencyCode = "UAH" даст правильне форматування автоматично.
Відносний час («2 години тому», «вчора»): RelativeDateTimeFormatter (iOS 13+) та RelativeTimeFormatter через intl пакет на Flutter/Android—не вигадуйте велосипед з ручним форматуванням.
Типові помилки при локалізації
Конкатенація строк замість форматування: "Hello, " + name + "!" працює для SVO-мов, але в японській ім'я йде перед ображенням. String(format: "greeting %@", name) з greeting = "%@ さん、こんにちは" в японському файлі—правильно.
Фіксований розмір UI під текст. Німецька в середньому на 30% довша за англійську. AutoLayout з правильними констрейнтами, adjustsFontSizeToFitWidth де допустимо, динамічна зміна висоти ячейк через UITableView.automaticDimension.
Зображення з embedded текстом—вимагають локалізованих версій або заміни на text overlay.
Графік
Додавання однієї нової мови в уже локалізований додаток (тільки переклад строк, без RTL)—2–3 дні технічної роботи + час перекладу. Первинна настройка інфраструктури локалізації з нуля, включаючи RTL-підтримку та динамічне переключення мови—2–4 тижні.







