Інтернаціоналізація та доступність
Вступ: культурний контекст та інклюзивність у мобільних застосунках
Мобільні додатки перебувають на перетині двох фундаментальних векторів цифрової інклюзивності: інтернаціоналізації (internationalization, i18n) — технічної адаптації продукту до мовно-культурних особливостей різних регіонів світу, та доступності (accessibility, a11y) — архітектурного забезпечення рівного доступу до функціональності для всіх категорій користувачів незалежно від сенсорних або когнітивних обмежень.
На відміну від веб-середовища, де браузер надає вбудовану підтримку доступності через єдиний інтерфейс Web Accessibility API (ARIA), мобільні застосунки безпосередньо інтегруються з нативними фреймворками операційних систем: VoiceOver (iOS), TalkBack (Android), Dynamic Type (адаптивна типографіка), Reduce Motion (зниження анімацій) та висококонтрастні режими.
Коректна реалізація інтернаціоналізації та доступності — це не опційна функціональність чи поліпшення для «нішової аудиторії». Це архітектурна вимога першого класу, закладена в міжнародні стандарти проєктування (WCAG 2.1 / 2.2), законодавство США (ADA Section 508), Європейського Союзу (European Accessibility Act) та вимоги платформ-дистриб'юторів (Apple App Store Review Guidelines пункт 4.2.1, Google Play Accessibility Requirements).
Мета лекції
🌍 Інтернаціоналізація (i18n)
- Розібрати архітектурні відмінності між статичним переведенням компіляційного часу (Compile-Time Translation) та динамічним завантаженням локалі (Runtime Locale Switching).
- Зрозуміти структуру бібліотеки react-i18next: сепарація перекладів через namespaces, інтерполяція змінних, плюралізація (plurals), форматування дат/чисел через
Intl APIта адаптація до мов із зворотним письмом (Right-to-Left, RTL). - Побудувати масштабовану систему перекладів із відокремленням рядків локалізації від бізнес-логіки.
♿ Доступність (a11y)
- Навчитися інтегрувати застосунок із системними асистивними технологіями: Screen Readers (читачі екрану), Switch Control (керування перемикачами), Voice Control (голосове управління) через атрибути
accessibilityLabel,accessibilityRole,accessibilityHintтаaccessibilityState. - Зрозуміти принципи семантичної доступності: Perceivable (сприйнятність), Operable (керованість), Understandable (зрозумілість) та Robust (надійність).
- Реалізувати підтримку динамічних розмірів шрифту (Dynamic Font Scaling) через підписку на
UIContentSizeCategory(iOS) та зовнішні налаштування (Settings → Display → Font Size). - Налаштувати контрастність інтерфейсу згідно стандарту WCAG AA (4.5:1 для звичайного тексту, 3:1 для великого заголовка) та адаптувати застосунок до режиму High Contrast Mode.
Частина I: Інтернаціоналізація (i18n) у React Native
Архітектура i18n: компіляційний час проти динамічної локалізації
Інтернаціоналізація ділиться на два фундаментально різні підходи реалізації:
- Статична локалізація (Compile-Time i18n): рядки різними мовами представлені окремими вихідними файлами (
strings.xmlдля Android,Localizable.stringsдля iOS), які компілюються безпосередньо в бінарний артефакт застосунку. Кожна підтримувана мова збільшує розмір.apkабо.ipa. Перемикання локалі вимагає перезапуску застосунку через зміну системних налаштувань операційної системи. - Динамічна локалізація (Runtime i18n): переклади зберігаються у форматах даних (
JSON,YAML,gettext .po), завантажуються в оперативну пам'ять під час виконання (at runtime) і можуть перемикатися без перезавантаження застосунку. Застосунок містить лише стандартну систему завантаження перекладів, а самі мовні пакети можуть оновлюватися незалежно через Over-The-Air Updates (OTA) або дозавантажуватися з CDN для рідкісних мов.
React Native екосистема спирається на другий підхід через бібліотеку react-i18next — офіційну React-інтеграцію для фреймворку i18next, що є галузевим стандартом динамічної локалізації в JavaScript-середовищі.
i18next не є специфічною для React Native — вона використовується у веб-додатках (через react-i18next), серверних застосунках Node.js, та навіть у нативних iOS/Android проєктах через відповідні адаптери. Підтримка обмежена полівером Intl API у версії JavaScriptCore (iOS 10+, Android через jsc-android >= 250230).Концептуальна модель react-i18next
Бібліотека базується на трьох базових сутностях:
+-------------------------------------------------------------------+
| АРХІТЕКТУРА react-i18next |
+-------------------------------------------------------------------+
| i18next.init() | Ініціалізація системи перекладів. |
| (Configuration Layer) | Конфігурація мов, namespaces, |
| | fallback, інтерполяції, плюралізації.|
+-------------------------------------------------------------------+
| Backend Plugin | Джерело завантаження перекладів: |
| (Data Loading Layer) | статичний import, локальна FS, |
| | або віддалений HTTP ендпоінт. |
+-------------------------------------------------------------------+
| React Hook: useTranslation() | Доступ до функції `t()` у |
| (Presentation Layer) | компонентах через хук. |
+-------------------------------------------------------------------+
Шлях рядка локалізації від файлу до інтерфейсу:
Встановлення та базова конфігурація
Бібліотека складається з двох пакетів: базовий i18next (логіка перекладів) та React-адаптер react-i18next.
npx expo install i18next react-i18next
npm install i18next react-i18next
yarn add i18next react-i18next
Структура проєкту з перекладами:
compatibilityJSON: 'v3' є критичним для React Native. Формат v4 використовує вкладені ключі JSON через крапку ("user.name": "Ім'я"), які некоректно парсяться рушієм Hermes. Завжди використовуйте версію v3 або явну структуру вкладених об'єктів.Організація перекладів через Namespaces
У реальних застосунках кількість рядків локалізації досягає сотень або тисяч одиниць. Збереження всіх перекладів в одному JSON-файлі призводить до проблем масштабування: конфлікти імен ключів, низька читабельність, неефективне завантаження в оперативну пам'ять.
Бібліотека i18next вирішує цю проблему через концепцію namespaces (просторів імен) — логічного розділення перекладів на окремі модулі за функціональною або екранною ознакою.
Типова структура namespaces у масштабному застосунку:
locales/
├── en/
│ ├── common.json # Глобальні терміни (кнопки, статуси, помилки)
│ ├── auth.json # Екран автентифікації
│ ├── profile.json # Екран профілю користувача
│ ├── errors.json # Повідомлення про помилки API
│ └── validation.json # Повідомлення валідації форм
└── uk/
├── common.json
├── auth.json
├── profile.json
├── errors.json
└── validation.json
Технічно можна зберігати всі переклади у одному файлі через вкладеність:
{
"common": { "welcome": "Welcome" },
"auth": { "login": "Log in" }
}
Проте такий підхід має критичні недоліки:
- Відсутність lazy loading: React Native завантажує весь JSON-файл в оперативну пам'ять під час ініціалізації, навіть якщо користувач перебуває на екрані профілю та не потребує перекладів екрану аутентифікації.
- Конфлікти при командній розробці: різні розробники одночасно редагують один файл, що призводить до git-конфліктів у merge request.
- Відсутність типізації: TypeScript не може згенерувати окремі типи для кожного namespace, що унеможливлює автокомпліт.
Конфігурація з множинними namespaces:
t('common:logout') замість t('logout'). Це дозволяє оперувати глобальними термінами (кнопки, статуси) з будь-якого компонента без додаткового виклику хука.Інтерполяція змінних та контекстна локалізація
Реальні рядки локалізації рідко бувають статичними. Найчастіше текст містить динамічні фрагменти: ім'я користувача, кількість елементів, дати, суми грошових коштів. Бібліотека i18next підтримує інтерполяцію змінних через шаблони подвійних фігурних дужок {{variable}}.
Базова інтерполяція
{
"greeting": "Hello, {{name}}!",
"itemsCount": "You have {{count}} items in your cart"
}
{
"greeting": "Привіт, {{name}}!",
"itemsCount": "У вашому кошику {{count}} товарів"
}
Використання інтерполяції в компоненті:
import { useTranslation } from 'react-i18next';
import { Text } from 'react-native';
export default function WelcomeScreen({ userName }: { userName: string }) {
const { t } = useTranslation('common');
return (
<Text style={{ fontSize: 18 }}>
{t('greeting', { name: userName })}
</Text>
);
}
// ❌ АНТИПАТЕРН: Порушення граматичних правил різних мов
<Text>{t('hello') + ' ' + userName + ', ' + t('welcome')}</Text>
// ✅ ПРАВИЛЬНО: Інтерполяція зі збереженням контексту
<Text>{t('greeting', { name: userName })}</Text>
Форматування чисел через Intl API
JavaScript надає стандартний API форматування чисел, валют та дат — Intl (Internationalization API). Цей API автоматично адаптує формат під поточну локаль користувача.
import { useTranslation } from 'react-i18next';
import { Text } from 'react-native';
export default function PriceDisplay({ amount }: { amount: number }) {
const { t, i18n } = useTranslation();
const formattedPrice = new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: 'USD',
}).format(amount);
return (
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>
{formattedPrice}
</Text>
);
}
Результат локалізації:
| Локаль | Вхід | Вихід |
|---|---|---|
en-US | 1234.56 | $1,234.56 |
de-DE | 1234.56 | 1.234,56 $ |
uk-UA | 1234.56 | 1 234,56 $ |
Intl через поліфіл @formatjs/intl-numberformat для старих версій JavaScriptCore. Expo автоматично включає цей поліфіл. У чистому CLI-проєкті потрібно встановити пакет вручну та імпортувати у точці входу: import '@formatjs/intl-numberformat/polyfill'.Плюралізація: коректна обробка множини
Більшість мов світу мають складні правила узгодження числівників із іменниками. Англійська мова використовує просту дихотомію однина/множина (singular/plural), але українська мова має три форми множини, а арабська — шість.
Бібліотека i18next реалізує стандарт Unicode CLDR Plural Rules, який автоматично обирає правильну форму слова залежно від числового значення та мовних правил локалі.
Правила плюралізації української мови
Українська мова належить до слов'янської групи із трьома формами множини:
| Форма | Умова | Приклад |
|---|---|---|
| one (однина) | Числа, що закінчуються на 1 (крім 11) | 1 товар, 21 товар, 101 товар |
| few (декілька) | Числа, що закінчуються на 2, 3, 4 (крім 12–14) | 2 товари, 3 товари, 23 товари |
| many (багато) | Всі інші числа | 5 товарів, 11 товарів, 100 товарів |
{
"itemsInCart_one": "{{count}} item",
"itemsInCart_other": "{{count}} items"
}
{
"itemsInCart_one": "{{count}} товар",
"itemsInCart_few": "{{count}} товари",
"itemsInCart_many": "{{count}} товарів"
}
Використання плюралізації:
import { useTranslation } from 'react-i18next';
import { Text } from 'react-native';
export default function CartSummary({ itemCount }: { itemCount: number }) {
const { t } = useTranslation('common');
return (
<Text style={{ fontSize: 16 }}>
{t('itemsInCart', { count: itemCount })}
</Text>
);
}
Результати для різних значень:
itemCount = 1→ "1 товар"itemCount = 2→ "2 товари"itemCount = 5→ "5 товарів"itemCount = 21→ "21 товар"
itemCount = 1→ "1 item"itemCount = 2→ "2 items"itemCount = 5→ "5 items"itemCount = 21→ "21 items"
Алгоритм базується на специфікації Unicode CLDR Plural Rules. Кожна мова має зареєстровану функцію визначення форми множини. Для української мови це правило виглядає наступним чином:
function pluralRule(n) {
const i = Math.floor(Math.abs(n));
const v = (n.toString().split('.')[1] || '').length;
if (v === 0 && i % 10 === 1 && i % 100 !== 11) return 'one';
if (v === 0 && i % 10 >= 2 && i % 10 <= 4 && (i % 100 < 12 || i % 100 > 14)) return 'few';
return 'many';
}
Бібліотека i18next автоматично застосовує відповідне правило під час виконання виклику t('key', { count: n }).
// ❌ АНТИПАТЕРН: Хардкод логіки плюралізації
const text = itemCount === 1 ? t('item') : t('items');
// ✅ ПРАВИЛЬНО: Делегування логіки бібліотеці
const text = t('itemsInCart', { count: itemCount });
Підтримка мов із зворотним письмом (RTL)
Близько 12% населення планети використовують мови із зворотним напрямком письма (Right-to-Left, RTL): арабська, іврит, перська, урду. Для таких мов інтерфейс повинен дзеркально перевернутися: текст вирівнюється праворуч, іконки навігації змінюють напрямок, послідовність елементів інвертується.
React Native надає вбудовану підтримку RTL через модуль I18nManager, який керує глобальним напрямком розташування елементів.
Активація RTL режиму
I18nManager.forceRTL() не застосовується миттєво. Нативні компоненти вимагають повного перезапуску застосунку для коректної зміни напрямку розташування. У продакшн-середовищі рекомендується виводити діалогове вікно з пропозицією перезапуску або автоматичне закриття через RNRestart.Restart() (пакет react-native-restart).Адаптивні стилі для RTL
Для коректної адаптації інтерфейсу використовуйте логічні властивості розташування замість абсолютних:
const styles = StyleSheet.create({
container: {
paddingLeft: 16, // Завжди зліва
marginRight: 12, // Завжди справа
},
});
const styles = StyleSheet.create({
container: {
paddingStart: 16, // Зліва для LTR, справа для RTL
marginEnd: 12, // Справа для LTR, зліва для RTL
},
});
Мап-таблиця логічних властивостей:
| Жорстка властивість | Логічний еквівалент | Поведінка в RTL |
|---|---|---|
paddingLeft | paddingStart | Стає paddingRight |
paddingRight | paddingEnd | Стає paddingLeft |
marginLeft | marginStart | Стає marginRight |
marginRight | marginEnd | Стає marginLeft |
borderLeftWidth | borderStartWidth | Стає borderRightWidth |
left: 0 | start: 0 | Стає right: 0 |
scaleX(-1) для горизонтального перевороту:import { I18nManager, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
icon: {
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
});
Візуалізація впливу RTL на компоненти
Частина II: Доступність (Accessibility, a11y) у React Native
Фундаментальні принципи доступності: WCAG та POUR
Доступність мобільних застосунків базується на чотирьох фундаментальних принципах, сформульованих у стандарті Web Content Accessibility Guidelines (WCAG 2.1 / 2.2), адаптованих під нативні платформи через Apple Human Interface Guidelines та Google Material Design Accessibility:
- Perceivable (Сприйнятність): Інформація та компоненти інтерфейсу повинні бути представлені у формі, доступній для сприйняття користувачем незалежно від сенсорних обмежень. Це включає альтернативний текст для зображень, підтримку динамічних розмірів шрифту, достатню контрастність кольорів.
- Operable (Керованість): Користувач повинен мати можливість взаємодіяти з усіма функціями застосунку незалежно від способу введення — сенсорний екран, зовнішня клавіатура, голосове керування (Voice Control), перемикачі (Switch Control).
- Understandable (Зрозумілість): Інформація та принципи роботи інтерфейсу повинні бути передбачуваними. Користувач не повинен здогадуватися про призначення елемента чи результат дії.
- Robust (Надійність): Контент повинен коректно інтерпретуватися різними асистивними технологіями, включаючи майбутні версії операційних систем та сторонні пристрої адаптивного керування.
📱 iOS: VoiceOver
🤖 Android: TalkBack
⌨️ Switch Control
🗣️ Voice Control
Архітектура асистивних технологій у React Native
На відміну від веб-середовища, де ARIA-атрибути транслюються через Accessibility Tree браузера, React Native безпосередньо інтегрується з нативними API доступності операційних систем:
Шлях інформації від компонента до VoiceOver:
- Розробник додає атрибут
accessibilityLabel="Login button"до компонентаPressable. - React Native Bridge транслює цей атрибут у нативну властивість
UIAccessibilityElement.accessibilityLabel(iOS) абоAccessibilityNodeInfo.setText()(Android). - Операційна система автоматично додає елемент у Accessibility Tree — паралельну ієрархію інтерфейсу, оптимізовану для навігації асистивними технологіями.
- Коли користувач активує VoiceOver і фокус потрапляє на кнопку, система озвучує: "Login button. Button. Double tap to activate."
<button> автоматично отримує роль button та вбудовану доступність, у React Native всі компоненти за замовчуванням недоступні для Screen Reader. Розробник повинен явно анотувати інтерфейс атрибутами доступності.Базові атрибути доступності
React Native надає набір атрибутів для інтеграції з системами доступності. Найважливіші з них: accessibilityLabel, accessibilityRole, accessibilityHint, accessibilityState.
accessibilityLabel: семантичний опис елемента
Атрибут accessibilityLabel визначає текст, який буде озвучений читачем екрана замість візуального контенту елемента. Це найважливіший атрибут доступності.
<Pressable onPress={handleLogin}>
<Text>→</Text>
</Pressable>
// VoiceOver озвучить: "Right arrow" — користувач не розуміє призначення
<Pressable
onPress={handleLogin}
accessibilityLabel="Log in to your account"
>
<Text>→</Text>
</Pressable>
// VoiceOver озвучить: "Log in to your account. Button."
Правила формулювання accessibilityLabel:
- Опишіть призначення, а не зовнішній вигляд:
- ❌ "Blue button with arrow"
- ✅ "Submit order"
- Уникайте дублювання ролі:
- ❌ "Login button button" (слово "button" додається автоматично)
- ✅ "Login"
- Використовуйте контекстну інформацію:
- ❌ "5" (значення без контексту)
- ✅ "5 unread messages"
- Не перевантажуйте інформацією:
- ❌ "This button will log you into your account. Please tap it twice to activate."
- ✅ "Log in"
Компоненти <Text> та <TextInput> автоматично використовують свій вміст як accessibilityLabel. Явна анотація потрібна лише якщо візуальний текст відрізняється від бажаного озвучення:
// Автоматично доступно
<Text>Welcome back, John</Text>
// VoiceOver: "Welcome back, John"
// Скорочення тексту для Screen Reader
<Text accessibilityLabel="Welcome back">
Welcome back, JohnDoeWithVeryLongName
</Text>
// VoiceOver: "Welcome back"
accessibilityRole: семантична роль елемента
Атрибут accessibilityRole вказує тип елемента інтерфейсу, що дозволяє читачу екрана надати користувачу додатковий контекст про поведінку компонента.
Підтримувані ролі:
| Роль | Призначення | Озвучення VoiceOver/TalkBack |
|---|---|---|
button | Натискувана кнопка | "Button. Double tap to activate." |
link | Посилання на інший екран/URL | "Link" |
header | Заголовок секції | "Heading" (допомагає навігації за заголовками) |
search | Поле пошуку | "Search field" |
image | Зображення | "Image" |
imagebutton | Інтерактивне зображення | "Image button" |
adjustable | Елемент із змінним значенням (Slider) | "Adjustable. Swipe up or down to adjust value." |
checkbox | Чекбокс | "Checkbox. Checked/Unchecked." |
radio | Радіо-кнопка | "Radio button. Selected/Not selected." |
switch | Перемикач | "Switch. On/Off." |
text | Статичний текст | Озвучується без додаткових міток |
none | Елемент виключений із Accessibility Tree | Ігнорується читачем екрана |
Приклади використання:
import { View, Text, Pressable, Image } from 'react-native';
export default function ProfileHeader() {
return (
<View>
{/* Заголовок екрану */}
<Text
style={{ fontSize: 28, fontWeight: 'bold' }}
accessibilityRole="header"
>
Profile Settings
</Text>
{/* Інтерактивна кнопка */}
<Pressable
onPress={handleEdit}
accessibilityRole="button"
accessibilityLabel="Edit profile"
>
<Text>Edit</Text>
</Pressable>
{/* Декоративне зображення */}
<Image
source={require('./avatar.png')}
accessibilityRole="image"
accessibilityLabel="Profile avatar"
/>
</View>
);
}
header має критичне значення для навігації. Користувачі VoiceOver можуть використовувати жест "Rotor → Headings" для швидкого переходу між секціями застосунку, не слухаючи весь інтерфейс послідовно.accessibilityState: динамічний стан елемента
Атрибут accessibilityState передає інформацію про поточний стан інтерактивного елемента: чи вибраний він, чи активний, чи заблокований, чи розгорнутий тощо.
Підтримувані властивості стану:
| Властивість | Тип | Призначення | Озвучення |
|---|---|---|---|
disabled | boolean | Елемент недоступний для взаємодії | "Dimmed" (iOS), "Disabled" (Android) |
selected | boolean | Елемент вибраний (списки, вкладки) | "Selected" / "Not selected" |
checked | boolean | 'mixed' | Стан чекбоксу | "Checked" / "Not checked" / "Mixed" |
busy | boolean | Елемент завантажується | "Busy" (затримує озвучення до завершення) |
expanded | boolean | Згортуваний елемент розгорнутий | "Expanded" / "Collapsed" |
Приклад: Вкладки навігації із станом:
import { Pressable, Text } from 'react-native';
type TabButtonProps = {
label: string;
isActive: boolean;
onPress: () => void;
};
function TabButton({ label, isActive, onPress }: TabButtonProps) {
return (
<Pressable
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected: isActive }}
style={{
padding: 12,
backgroundColor: isActive ? '#3b82f6' : '#e5e7eb',
}}
>
<Text style={{ color: isActive ? 'white' : '#1f2937' }}>
{label}
</Text>
</Pressable>
);
}
export default function TabNavigation() {
const [activeTab, setActiveTab] = React.useState('home');
return (
<>
<TabButton
label="Home"
isActive={activeTab === 'home'}
onPress={() => setActiveTab('home')}
/>
<TabButton
label="Profile"
isActive={activeTab === 'profile'}
onPress={() => setActiveTab('profile')}
/>
</>
);
}
Озвучення VoiceOver:
- Активна вкладка: "Home. Button. Selected."
- Неактивна вкладка: "Profile. Button. Not selected."
disabled:Якщо кнопка візуально неактивна (сіра, напівпрозора), але атрибут accessibilityState={{ disabled: true }} не встановлений, користувач Screen Reader спробує натиснути її і отримає непередбачувану поведінку або мовчазний збій.// ❌ НЕПРАВИЛЬНО: Візуально заблокована, але доступна для Screen Reader
<Pressable style={{ opacity: 0.5 }} onPress={undefined}>
<Text>Submit</Text>
</Pressable>
// ✅ ПРАВИЛЬНО: Заблокована і для Screen Reader
<Pressable
style={{ opacity: 0.5 }}
onPress={undefined}
accessibilityState={{ disabled: true }}
accessibilityLabel="Submit (disabled, fill in all fields)"
>
<Text>Submit</Text>
</Pressable>
accessibilityHint: додаткова контекстна підказка
Атрибут accessibilityHint надає додаткову інформацію про результат виконання дії. На відміну від accessibilityLabel, який описує що це, hint пояснює що станеться після натискання.
<Pressable
accessibilityRole="button"
accessibilityLabel="Delete"
onPress={handleDelete}
>
<Text>🗑️</Text>
</Pressable>
// VoiceOver: "Delete. Button."
// Користувач не розуміє, що буде видалено
<Pressable
accessibilityRole="button"
accessibilityLabel="Delete"
accessibilityHint="Deletes this message permanently"
onPress={handleDelete}
>
<Text>🗑️</Text>
</Pressable>
// VoiceOver: "Delete. Button. Deletes this message permanently."
Правила формулювання hint:
- Починайте з дієслова в третій особі:
- ✅ "Opens settings menu"
- ❌ "Will open settings" / "Open settings"
- Описуйте наслідок, а не дію натискання:
- ✅ "Sends the message to all team members"
- ❌ "Tap to send message"
- Уникайте очевидного:
- Для кнопки "Close" hint "Closes the dialog" є надлишковим.
- Використовуйте hint для попередження про деструктивні дії:
- ✅ "Permanently deletes your account. This action cannot be undone."
accessibilityLabel, а не в hint.Семантичне групування елементів: accessible та accessibilityElementsHidden
Складні компоненти часто складаються з кількох вкладених елементів (View, Text, Image). За замовчуванням Screen Reader зупиняється на кожному елементі окремо, озвучуючи їх по черзі, що створює фрагментований досвід.
accessible={true}: групування елементів у одну семантичну одиницю
Атрибут accessible={true} на батьківському контейнері об'єднує всі вкладені елементи в один фокусний вузол.
Приклад: Картка продукту:
<View>
<Image source={{ uri: 'product.jpg' }} />
<Text>Wireless Headphones</Text>
<Text>$129.99</Text>
<Text>⭐ 4.8 (320 reviews)</Text>
</View>
// Screen Reader зупиняється 4 рази:
// 1. "Image"
// 2. "Wireless Headphones"
// 3. "One hundred twenty nine dollars ninety nine cents"
// 4. "Star. Four point eight. Three hundred twenty reviews"
<View
accessible={true}
accessibilityLabel="Wireless Headphones, $129.99, rated 4.8 stars by 320 customers"
accessibilityRole="button"
accessibilityHint="Opens product details"
>
<Image source={{ uri: 'product.jpg' }} />
<Text>Wireless Headphones</Text>
<Text>$129.99</Text>
<Text>⭐ 4.8 (320 reviews)</Text>
</View>
// Screen Reader озвучує один раз:
// "Wireless Headphones, one hundred twenty nine dollars ninety nine cents,
// rated four point eight stars by three hundred twenty customers.
// Button. Opens product details."
accessible={true} до інтерактивних карток, кнопок із іконками та текстом, елементів списків із складною структурою. Це радикально покращує швидкість навігації.accessibilityElementsHidden={true}: виключення декоративних елементів
Деякі елементи інтерфейсу є чисто декоративними і не несуть семантичного навантаження: фонові градієнти, декоративні іконки, розділові лінії. Їх озвучення створює інформаційний шум.
Атрибут accessibilityElementsHidden={true} повністю виключає елемент та всіх його нащадків із Accessibility Tree.
<View style={{ backgroundColor: '#f3f4f6', padding: 20 }}>
{/* Декоративна іконка без семантики */}
<Image
source={require('./decorative-pattern.png')}
accessibilityElementsHidden={true}
/>
<Text accessibilityRole="header">
Welcome to the App
</Text>
{/* Горизонтальна лінія-роздільник */}
<View
style={{ height: 1, backgroundColor: '#e5e7eb' }}
accessibilityElementsHidden={true}
/>
</View>
iOS використовує: accessibilityElementsHidden={true}Android використовує: importantForAccessibility="no-hide-descendants"
React Native автоматично транслює ці атрибути на відповідну платформу, але для міжплатформної сумісності рекомендується використовувати accessibilityElementsHidden, який коректно працює на обох ОС.
Тонке розрізнення:
| Атрибут | Поведінка |
|---|---|
accessibilityElementsHidden={true} | Приховує елемент та всіх нащадків |
importantForAccessibility="no" (Android) | Приховує лише цей елемент, але нащадки доступні |
importantForAccessibility="no-hide-descendants" | Приховує елемент та нащадків (аналог iOS) |
Підтримка динамічних розмірів шрифту (Dynamic Type)
Значна частина користувачів змінює системний розмір шрифту через налаштування доступності для компенсації вікових змін зору або візуальних порушень. Операційні системи надають глобальну шкалу масштабування тексту:
- iOS: Settings → Display & Brightness → Text Size (діапазон від xSmall до AX5, де AX — accessibility sizes)
- Android: Settings → Display → Font Size (шкала від Small до Largest)
React Native компонент <Text> не адаптується автоматично до системних налаштувань шрифту. Розробник повинен явно реалізувати логіку масштабування через модуль PixelRatio або бібліотеку react-native-size-matters.
Проблема фіксованих розмірів шрифту
<Text style={{ fontSize: 16 }}>
By continuing, you agree to our Terms of Service
</Text>
// Текст залишається 16 пікселів незалежно від системних налаштувань
import { useWindowDimensions, PixelRatio } from 'react-native';
function ScaledText({ children, baseSize }: { children: string; baseSize: number }) {
const fontScale = PixelRatio.getFontScale();
const scaledSize = baseSize * fontScale;
return (
<Text style={{ fontSize: scaledSize }}>
{children}
</Text>
);
}
// Використання:
<ScaledText baseSize={16}>
By continuing, you agree to our Terms of Service
</ScaledText>
// Текст масштабується відповідно до системних налаштувань
Адаптивна типографічна шкала через хук
Для масштабованих застосунків рекомендується створити централізовану систему типографіки:
// ❌ Текст склеюється при великому шрифті
<Text style={{ fontSize: scaledSize }}>Long paragraph...</Text>
// ✅ Читабельний інтервал
<Text style={{ fontSize: scaledSize, lineHeight: scaledSize * 1.5 }}>
Long paragraph...
</Text>
Тестування з різними розмірами шрифту
Найпоширеніша помилка — дизайн інтерфейсу, який красиво виглядає зі стандартним шрифтом, але повністю ламається при збільшенні до максимального розміру (AX5 на iOS або Largest на Android): текст виходить за межі контейнерів, кнопки перекриваються, критичний контент стає недоступним.
- Відкрийте Settings.app у симуляторі
- Перейдіть до Accessibility → Display & Text Size → Larger Text
- Увімкніть Larger Accessibility Sizes
- Пересуньте повзунок до максимального значення AX5
- Перезапустіть застосунок і перевірте всі екрани
- Відкрийте Settings в емуляторі
- Перейдіть до Display → Font size
- Оберіть Largest
- Перезапустіть застосунок
- Альтернатива через ADB:
adb shell settings put system font_scale 2.0
- Уникайте фіксованих висот контейнерів: Використовуйте
minHeightзамістьheight. - Дозволяйте обгортання тексту:
flexWrap: 'wrap'для горизонтальних контейнерів. - Збільшуйте простір між елементами:
gapабоmarginVerticalмає масштабуватися пропорційно. - Застосовуйте ScrollView: Навіть для статичних екранів — користувач зможе прокрутити вміст, що виходить за межі.
Контрастність кольорів: WCAG стандарти
Достатня контрастність між текстом та фоном є критичною вимогою доступності. Стандарт WCAG визначає два рівні відповідності:
Мінімальні коефіцієнти контрастності:
| Рівень | Звичайний текст (<18pt) | Великий текст (≥18pt або ≥14pt bold) |
|---|---|---|
| WCAG AA (мінімальний) | 4.5:1 | 3:1 |
| WCAG AAA (покращений) | 7:1 | 4.5:1 |
Формула обчислення контрастності (Relative Luminance):
де Завантаження... — відносна яскравість світлішого кольору, Завантаження... — темнішого.
Валідація кольорової палітри
const styles = StyleSheet.create({
text: {
color: '#9ca3af', // Gray-400
backgroundColor: '#fff', // White
// Контрастність: 2.85:1 — не проходить WCAG AA
},
});
const styles = StyleSheet.create({
text: {
color: '#374151', // Gray-700
backgroundColor: '#fff', // White
// Контрастність: 10.8:1 — проходить WCAG AAA
},
});
Онлайн інструменти для перевірки контрастності:
- WebAIM Contrast Checker
- Contrast Ratio (Lea Verou)
- Плагін Figma: Stark або Color Blind
Підтримка режиму High Contrast Mode
Операційні системи надають режим підвищеної контрастності для користувачів із вадами зору:
- iOS: Settings → Accessibility → Display & Text Size → Increase Contrast
- Android: Settings → Accessibility → Visibility Enhancements → High Contrast
React Native не надає вбудованого API для детекції цього режиму. Для повноцінної підтримки використовуйте бібліотеку react-native-device-info або створіть власний Native Module.
Приклад адаптації кольорів:
import { useColorScheme } from 'react-native';
function useAccessibleColors() {
const colorScheme = useColorScheme();
// У продакшн-середовищі додайте детекцію High Contrast через Native Module
const isHighContrast = false; // Placeholder
if (isHighContrast) {
return {
text: colorScheme === 'dark' ? '#ffffff' : '#000000',
background: colorScheme === 'dark' ? '#000000' : '#ffffff',
border: colorScheme === 'dark' ? '#ffffff' : '#000000',
};
}
return {
text: colorScheme === 'dark' ? '#f3f4f6' : '#1f2937',
background: colorScheme === 'dark' ? '#1f2937' : '#ffffff',
border: colorScheme === 'dark' ? '#4b5563' : '#e5e7eb',
};
}
Програмне керування фокусом доступності
У деяких сценаріях розробник повинен явно перемістити фокус Screen Reader на конкретний елемент: після відкриття модального вікна, завершення асинхронної операції або відображення повідомлення про помилку.
React Native надає два API для керування фокусом:
AccessibilityInfo.setAccessibilityFocus(reactTag)— переміщує фокус Screen Reader на вказаний елемент.AccessibilityInfo.announceForAccessibility(announcement)— озвучує текстове повідомлення без зміни фокусу.
Переміщення фокусу після відкриття діалогу
accessibilityViewIsModal={true} є критичним для модальних вікон. Він інформує Screen Reader, що фокус повинен залишатися лише всередині цього контейнера, ігноруючи всі елементи фонового екрану. Без цього атрибута користувач може "прокрутити" фокус за межі діалогу і взаємодіяти з заблокованим інтерфейсом.Озвучення повідомлень без зміни фокусу
Метод announceForAccessibility() використовується для озвучення статусних повідомлень, які не вимагають переміщення фокусу: успішне збереження форми, відновлення мережевого з'єднання, завершення фонового завантаження.
import { AccessibilityInfo } from 'react-native';
async function saveUserProfile(data: UserProfile) {
try {
await api.updateProfile(data);
// Озвучення успіху без зміни фокусу
AccessibilityInfo.announceForAccessibility('Profile saved successfully');
} catch (error) {
AccessibilityInfo.announceForAccessibility('Failed to save profile. Please try again.');
}
}
Використовуйте setAccessibilityFocus:
- При відкритті модального вікна чи діалогу (фокус переміщується на заголовок).
- Після відправки форми з помилками валідації (фокус на першому невалідному полі).
- При динамічному завантаженні контенту, який змінює структуру сторінки.
Використовуйте announceForAccessibility:
- Повідомлення про успішне збереження даних.
- Зміна стану завантаження ("Loading..." → "Content loaded").
- Відновлення мережевого з'єднання.
- Оновлення прогрес-бару.
Тестування доступності з Accessibility Inspector
Apple та Google надають вбудовані інструменти для аудиту доступності під час розробки.
iOS: Accessibility Inspector (Xcode)
Активація інструменту:
- Відкрийте Xcode
- Меню: Xcode → Open Developer Tool → Accessibility Inspector
- Оберіть цільовий симулятор або фізичний пристрій у верхньому випадаючому меню
- Натисніть кнопку Inspection Pointer (іконка прицілу)
Функціонал інспектора:
Типові проблеми, що виявляються автоматичним аудитом:
| Помилка | Опис | Вирішення |
|---|---|---|
| Missing accessibility label | Інтерактивний елемент без опису | Додати accessibilityLabel |
| Redundant accessibility label | Label дублює текст компонента | Видалити атрибут або скоротити |
| Element not focusable | Кнопка недоступна для Screen Reader | Додати accessibilityRole="button" |
| Insufficient contrast | Контрастність < 4.5:1 | Змінити колір тексту або фону |
| Missing dynamic type support | Текст не масштабується | Реалізувати useScaledFontSize |
Android: Accessibility Scanner
Встановлення та запуск:
- Встановіть Accessibility Scanner з Google Play Store на фізичному пристрої або емуляторі.
- Активуйте сервіс: Settings → Accessibility → Accessibility Scanner → Use service.
- Відкрийте ваш застосунок і натисніть плаваючу кнопку сканера.
- Інструмент автоматично виділить проблемні елементи та надасть рекомендації.
Приклад звіту:
❌ Touch target size too small
Element: "×" close button
Current size: 32×32 dp
Minimum required: 48×48 dp
Suggestion: Increase padding or button dimensions
⚠️ Low contrast ratio
Element: "Sign up" link
Current: 3.2:1
Required: 4.5:1
Suggestion: Use #1e40af instead of #60a5fa
✅ All interactive elements have labels
Реальне тестування з VoiceOver та TalkBack
Єдиний спосіб гарантувати доступність — це пройти весь користувацький сценарій із увімкненим Screen Reader.
Базові жести VoiceOver (iOS):
| Жест | Дія |
|---|---|
| Свайп вправо | Наступний елемент |
| Свайп вліво | Попередній елемент |
| Подвійний тап | Активувати елемент |
| Три пальці свайп вгору/вниз | Прокрутка сторінки |
| Z-pattern (двома пальцями) | Відкрити Rotor (швидка навігація за типами) |
Базові жести TalkBack (Android):
| Жест | Дія |
|---|---|
| Свайп вправо | Наступний елемент |
| Свайп вліво | Попередній елемент |
| Подвійний тап | Активувати елемент |
| Свайп вниз-вправо одним рухом | Відкрити контекстне меню TalkBack |
| Два пальці свайп вгору/вниз | Прокрутка |
- ✅ Всі інтерактивні елементи мають зрозумілі labels?
- ✅ Порядок фокусу відповідає візуальній логіці екрану?
- ✅ Деструктивні дії (видалення, вихід) супроводжуються попередженнями?
- ✅ Помилки валідації озвучуються і фокус переміщується на проблемне поле?
- ✅ Модальні вікна обмежують фокус (
accessibilityViewIsModal)? - ✅ Декоративні елементи виключені з навігації?
- ✅ Текст залишається читабельним при максимальному розмірі шрифту (AX5)?
- ✅ Інтерфейс функціональний у режимі High Contrast?
Інтеграція i18n та a11y у реальному компоненті
Об'єднаємо отримані знання у повноцінний приклад: форма входу з підтримкою багатомовності, доступності, динамічних розмірів шрифту та коректної контрастності.
Аналіз доступності компонента
- Багатомовність: Всі рядки інтерфейсу винесені в JSON-файли перекладів із підтримкою інтерполяції.
- Динамічні розміри шрифту: Використання хука
useScaledFontSizeгарантує масштабування під системні налаштування. - Семантичні ролі: Кожен інтерактивний елемент має відповідний
accessibilityRole(button,link,header,alert). - Контрастність: Кольори тексту та фону відповідають стандарту WCAG AA (4.5:1 для звичайного тексту).
- Озвучення статусів: Повідомлення про помилку озвучується через
AccessibilityInfo.announceForAccessibility, фокус переміщається на текст помилки. - Мінімальні розміри сенсорних зон: Всі кнопки мають
minHeight: 48(відповідає стандарту Android Material Design). - Підказки для Screen Reader: Атрибути
accessibilityHintпояснюють результат дії. - Логічна послідовність введення:
returnKeyType="next"таonSubmitEditingзабезпечують плавну навігацію клавіатурою.
- Валідація в реальному часі: Додати помилки валідації під кожним полем із автоматичним переміщенням фокусу на перше невалідне поле.
- Підтримка RTL: Додати логічні властивості стилів (
paddingStartзамістьpaddingLeft) та детекцію напрямку письма черезI18nManager. - Тестування з Accessibility Inspector: Провести автоматичний аудит через Xcode Accessibility Inspector для виявлення прихованих проблем.
- Підтримка голосового керування: Додати явні
accessibilityLabelдля складних компонентів, щоб команда "Tap Login button" коректно спрацьовувала.
Підсумки: чеклист впровадження i18n та a11y
🌍 Інтернаціоналізація
- ✅ Всі рядки інтерфейсу винесені у JSON-файли перекладів
- ✅ Використання namespaces для логічного розділення перекладів
- ✅ Інтерполяція змінних замість конкатенації рядків
- ✅ Плюралізація через Unicode CLDR Plural Rules
- ✅ Форматування дат/чисел через
Intl API - ✅ Підтримка RTL через
I18nManager.forceRTL() - ✅ Логічні властивості стилів (
paddingStart,marginEnd)
♿ Доступність
- ✅ Всі інтерактивні елементи мають
accessibilityLabel - ✅ Коректні семантичні ролі (
accessibilityRole) - ✅ Динамічні стани через
accessibilityState - ✅ Контекстні підказки через
accessibilityHint - ✅ Семантичне групування через
accessible={true} - ✅ Виключення декоративних елементів (
accessibilityElementsHidden) - ✅ Підтримка динамічних розмірів шрифту
- ✅ Контрастність ≥ 4.5:1 (WCAG AA)
- ✅ Мінімальні touch targets 48×48 dp / 44×44 pt
- ✅ Програмне керування фокусом у критичних сценаріях
- ✅ Тестування з VoiceOver / TalkBack
- ✅ Автоматичний аудит через Accessibility Inspector
@testing-library/react-native із правилами eslint-plugin-jsx-a11y).Додаткові ресурси
Push і локальні сповіщення
Локальні нагадування, Android канали, обробка тапу і навігація до потрібного екрана у React Native / Expo Notifications
EAS Build та профілі збірки
Повний покроковий гід зі збірки React Native застосунків через Expo Application Services (EAS Build) — від реєстрації облікового запису до отримання готових APK/IPA файлів. Налаштування профілів збірки, змінних оточення, підписування сертифікатів та автоматизація через CI/CD