Version2

Інтернаціоналізація та доступність

Повний гід з i18n через react-i18next (namespaces, plurals, interpolation, RTL) та доступності в React Native (Screen Reader, accessibilityLabel, Role, динамічні розміри шрифту, Color Contrast, керування фокусом, Testing з Accessibility Inspector)

Вступ: культурний контекст та інклюзивність у мобільних застосунках

Мобільні додатки перебувають на перетині двох фундаментальних векторів цифрової інклюзивності: інтернаціоналізації (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: компіляційний час проти динамічної локалізації

Інтернаціоналізація ділиться на два фундаментально різні підходи реалізації:

  1. Статична локалізація (Compile-Time i18n): рядки різними мовами представлені окремими вихідними файлами (strings.xml для Android, Localizable.strings для iOS), які компілюються безпосередньо в бінарний артефакт застосунку. Кожна підтримувана мова збільшує розмір .apk або .ipa. Перемикання локалі вимагає перезапуску застосунку через зміну системних налаштувань операційної системи.
  2. Динамічна локалізація (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)    |  компонентах через хук.               |
+-------------------------------------------------------------------+

Шлях рядка локалізації від файлу до інтерфейсу:

Loading diagram...
flowchart LR
    A["JSON файл<br/>en/common.json"] --> B["i18next.init()<br/>завантаження"]
    B --> C["I18nextProvider<br/>контекст"]
    C --> D["useTranslation()<br/>хук у компоненті"]
    D --> E["t('common:welcome')<br/>інтерпольований рядок"]
    E --> F["<Text>{t(...)}</Text><br/>UI"]
    
    style A fill:#DBEAFE,stroke:#1E293B,color:#1E293B
    style B fill:#E2E8F0,stroke:#1E293B,color:#1E293B
    style C fill:#E2E8F0,stroke:#1E293B,color:#1E293B
    style D fill:#E2E8F0,stroke:#1E293B,color:#1E293B
    style E fill:#DCFCE7,stroke:#1E293B,color:#1E293B
    style F fill:#FEF3C7,stroke:#1E293B,color:#1E293B

Встановлення та базова конфігурація

Бібліотека складається з двох пакетів: базовий i18next (логіка перекладів) та React-адаптер react-i18next.

npx expo install 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

Конфігурація з множинними namespaces:

Якщо потрібно використати переклад з іншого namespace без зміни поточного контексту, застосовуйте префікс: t('common:logout') замість t('logout'). Це дозволяє оперувати глобальними термінами (кнопки, статуси) з будь-якого компонента без додаткового виклику хука.

Інтерполяція змінних та контекстна локалізація

Реальні рядки локалізації рідко бувають статичними. Найчастіше текст містить динамічні фрагменти: ім'я користувача, кількість елементів, дати, суми грошових коштів. Бібліотека i18next підтримує інтерполяцію змінних через шаблони подвійних фігурних дужок {{variable}}.

Базова інтерполяція

{
  "greeting": "Hello, {{name}}!",
  "itemsCount": "You have {{count}} items in your cart"
}

Використання інтерполяції в компоненті:

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>
  );
}
Небезпека concatenation рядків: Типова помилка початківців — спроба конкатенації рядків замість інтерполяції:
// ❌ АНТИПАТЕРН: Порушення граматичних правил різних мов
<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-US1234.56$1,234.56
de-DE1234.561.234,56 $
uk-UA1234.561 234,56 $
React Native підтримує API 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"
}

Використання плюралізації:

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 товар"
Поширена помилка: ручна реалізація умов:
// ❌ АНТИПАТЕРН: Хардкод логіки плюралізації
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 режиму

Перезапуск застосунку для RTL: Зміна режиму RTL через I18nManager.forceRTL() не застосовується миттєво. Нативні компоненти вимагають повного перезапуску застосунку для коректної зміни напрямку розташування. У продакшн-середовищі рекомендується виводити діалогове вікно з пропозицією перезапуску або автоматичне закриття через RNRestart.Restart() (пакет react-native-restart).

Адаптивні стилі для RTL

Для коректної адаптації інтерфейсу використовуйте логічні властивості розташування замість абсолютних:

const styles = StyleSheet.create({
  container: {
    paddingLeft: 16,   // Завжди зліва
    marginRight: 12,   // Завжди справа
  },
});

Мап-таблиця логічних властивостей:

Жорстка властивістьЛогічний еквівалентПоведінка в RTL
paddingLeftpaddingStartСтає paddingRight
paddingRightpaddingEndСтає paddingLeft
marginLeftmarginStartСтає marginRight
marginRightmarginEndСтає marginLeft
borderLeftWidthborderStartWidthСтає borderRightWidth
left: 0start: 0Стає right: 0
Іконки з семантичним напрямком (стрілки, меню) потрібно автоматично дзеркалити. Використовуйте трансформацію scaleX(-1) для горизонтального перевороту:
import { I18nManager, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  icon: {
    transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
  },
});

Візуалізація впливу RTL на компоненти

Loading diagram...
flowchart LR
    subgraph LTR ["LTR Layout (English)"]
        direction LR
        A1["☰ Menu"] --> B1["Screen Title"]
        B1 --> C1["→ Next"]
    end
    
    subgraph RTL ["RTL Layout (Arabic)"]
        direction RL
        C2["← التالي"] --> B2["عنوان الشاشة"]
        B2 --> A2["☰ القائمة"]
    end
    
    style LTR fill:#E2E8F0,stroke:#1E293B,color:#1E293B
    style RTL fill:#DBEAFE,stroke:#1E293B,color:#1E293B

Частина II: Доступність (Accessibility, a11y) у React Native

Фундаментальні принципи доступності: WCAG та POUR

Доступність мобільних застосунків базується на чотирьох фундаментальних принципах, сформульованих у стандарті Web Content Accessibility Guidelines (WCAG 2.1 / 2.2), адаптованих під нативні платформи через Apple Human Interface Guidelines та Google Material Design Accessibility:

  1. Perceivable (Сприйнятність): Інформація та компоненти інтерфейсу повинні бути представлені у формі, доступній для сприйняття користувачем незалежно від сенсорних обмежень. Це включає альтернативний текст для зображень, підтримку динамічних розмірів шрифту, достатню контрастність кольорів.
  2. Operable (Керованість): Користувач повинен мати можливість взаємодіяти з усіма функціями застосунку незалежно від способу введення — сенсорний екран, зовнішня клавіатура, голосове керування (Voice Control), перемикачі (Switch Control).
  3. Understandable (Зрозумілість): Інформація та принципи роботи інтерфейсу повинні бути передбачуваними. Користувач не повинен здогадуватися про призначення елемента чи результат дії.
  4. Robust (Надійність): Контент повинен коректно інтерпретуватися різними асистивними технологіями, включаючи майбутні версії операційних систем та сторонні пристрої адаптивного керування.

📱 iOS: VoiceOver

Вбудований читач екрана iOS. Активується через Settings → Accessibility → VoiceOver. Озвучує всі елементи інтерфейсу при фокусі. Жести навігації: свайп вправо/вліво для переміщення фокусу, подвійне натискання для активації елемента.

🤖 Android: TalkBack

Офіційний читач екрана Android. Активується через Settings → Accessibility → TalkBack. Аналогічна система жестів: свайп для переміщення фокусу, подвійний тап для дії. Підтримує різні режими навігації: лінійна, за заголовками, за елементами керування.

⌨️ Switch Control

Режим керування через зовнішній адаптивний перемикач (для людей із моторними обмеженнями). Система автоматично сканує інтерфейс, підсвічуючи елементи послідовно. Користувач активує потрібний елемент натисканням перемикача.

🗣️ Voice Control

Голосове керування інтерфейсом без використання рук. Дозволяє озвучувати команди на кшталт "Tap Login button", "Scroll down", "Show numbers" (накладає числові мітки на всі інтерактивні елементи для прямого виклику).

Архітектура асистивних технологій у React Native

На відміну від веб-середовища, де ARIA-атрибути транслюються через Accessibility Tree браузера, React Native безпосередньо інтегрується з нативними API доступності операційних систем:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

package "React Native Layer" #E2E8F0 {
  [View + Text] as RN
  [accessibilityLabel] as Props
}

package "Native Bridge" #DBEAFE {
  [iOS Native Modules] as iOS
  [Android Native Modules] as Android
}

package "OS Accessibility APIs" #DCFCE7 {
  [UIAccessibility (iOS)] as iOSAPI
  [AccessibilityNodeInfo (Android)] as AndroidAPI
}

package "Assistive Technologies" #FEF3C7 {
  [VoiceOver] as VO
  [TalkBack] as TB
  [Switch Control] as SC
}

RN --> Props
Props --> iOS
Props --> Android

iOS --> iOSAPI
Android --> AndroidAPI

iOSAPI --> VO
iOSAPI --> SC
AndroidAPI --> TB

note right of Props
  Атрибути React Native:
  • accessibilityLabel
  • accessibilityRole
  • accessibilityHint
  • accessibilityState
end note

@enduml

Шлях інформації від компонента до VoiceOver:

  1. Розробник додає атрибут accessibilityLabel="Login button" до компонента Pressable.
  2. React Native Bridge транслює цей атрибут у нативну властивість UIAccessibilityElement.accessibilityLabel (iOS) або AccessibilityNodeInfo.setText() (Android).
  3. Операційна система автоматично додає елемент у Accessibility Tree — паралельну ієрархію інтерфейсу, оптимізовану для навігації асистивними технологіями.
  4. Коли користувач активує 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" — користувач не розуміє призначення

Правила формулювання accessibilityLabel:

  1. Опишіть призначення, а не зовнішній вигляд:
    • ❌ "Blue button with arrow"
    • ✅ "Submit order"
  2. Уникайте дублювання ролі:
    • ❌ "Login button button" (слово "button" додається автоматично)
    • ✅ "Login"
  3. Використовуйте контекстну інформацію:
    • ❌ "5" (значення без контексту)
    • ✅ "5 unread messages"
  4. Не перевантажуйте інформацією:
    • ❌ "This button will log you into your account. Please tap it twice to activate."
    • ✅ "Log in"

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 передає інформацію про поточний стан інтерактивного елемента: чи вибраний він, чи активний, чи заблокований, чи розгорнутий тощо.

Підтримувані властивості стану:

ВластивістьТипПризначенняОзвучення
disabledbooleanЕлемент недоступний для взаємодії"Dimmed" (iOS), "Disabled" (Android)
selectedbooleanЕлемент вибраний (списки, вкладки)"Selected" / "Not selected"
checkedboolean | 'mixed'Стан чекбоксу"Checked" / "Not checked" / "Mixed"
busybooleanЕлемент завантажується"Busy" (затримує озвучення до завершення)
expandedbooleanЗгортуваний елемент розгорнутий"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."
// Користувач не розуміє, що буде видалено

Правила формулювання hint:

  1. Починайте з дієслова в третій особі:
    • ✅ "Opens settings menu"
    • ❌ "Will open settings" / "Open settings"
  2. Описуйте наслідок, а не дію натискання:
    • ✅ "Sends the message to all team members"
    • ❌ "Tap to send message"
  3. Уникайте очевидного:
    • Для кнопки "Close" hint "Closes the dialog" є надлишковим.
  4. Використовуйте hint для попередження про деструктивні дії:
    • ✅ "Permanently deletes your account. This action cannot be undone."
Користувачі можуть глобально вимкнути озвучення hints через налаштування доступності (Settings → VoiceOver → Verbosity → Hints). Критична інформація завжди повинна бути в 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"
Завжди додавайте 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>

Підтримка динамічних розмірів шрифту (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 пікселів незалежно від системних налаштувань

Адаптивна типографічна шкала через хук

Для масштабованих застосунків рекомендується створити централізовану систему типографіки:

Критична помилка: ігнорування висоти рядка (line-height):Під час масштабування шрифту пропорційно має збільшуватися і міжрядковий інтервал (line-height). Стандарт WCAG рекомендує мінімум 1.5× від розміру шрифту для звичайного тексту та 1.2× для заголовків.
// ❌ Текст склеюється при великому шрифті
<Text style={{ fontSize: scaledSize }}>Long paragraph...</Text>

// ✅ Читабельний інтервал
<Text style={{ fontSize: scaledSize, lineHeight: scaledSize * 1.5 }}>
  Long paragraph...
</Text>

Тестування з різними розмірами шрифту

Найпоширеніша помилка — дизайн інтерфейсу, який красиво виглядає зі стандартним шрифтом, але повністю ламається при збільшенні до максимального розміру (AX5 на iOS або Largest на Android): текст виходить за межі контейнерів, кнопки перекриваються, критичний контент стає недоступним.

  1. Відкрийте Settings.app у симуляторі
  2. Перейдіть до Accessibility → Display & Text Size → Larger Text
  3. Увімкніть Larger Accessibility Sizes
  4. Пересуньте повзунок до максимального значення AX5
  5. Перезапустіть застосунок і перевірте всі екрани
Стратегія адаптивного дизайну при великих шрифтах:
  1. Уникайте фіксованих висот контейнерів: Використовуйте minHeight замість height.
  2. Дозволяйте обгортання тексту: flexWrap: 'wrap' для горизонтальних контейнерів.
  3. Збільшуйте простір між елементами: gap або marginVertical має масштабуватися пропорційно.
  4. Застосовуйте ScrollView: Навіть для статичних екранів — користувач зможе прокрутити вміст, що виходить за межі.

Контрастність кольорів: WCAG стандарти

Достатня контрастність між текстом та фоном є критичною вимогою доступності. Стандарт WCAG визначає два рівні відповідності:

Мінімальні коефіцієнти контрастності:

РівеньЗвичайний текст (<18pt)Великий текст (≥18pt або ≥14pt bold)
WCAG AA (мінімальний)4.5:13:1
WCAG AAA (покращений)7:14.5:1

Формула обчислення контрастності (Relative Luminance):

Завантаження...
\text{Contrast Ratio} = \frac{L_1 + 0.05}{L_2 + 0.05}

де Завантаження... — відносна яскравість світлішого кольору, Завантаження... — темнішого.

Валідація кольорової палітри

const styles = StyleSheet.create({
  text: {
    color: '#9ca3af',      // Gray-400
    backgroundColor: '#fff', // White
    // Контрастність: 2.85:1 — не проходить WCAG AA
  },
});

Онлайн інструменти для перевірки контрастності:

Підтримка режиму 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',
  };
}
Apple Human Interface Guidelines вимагає, щоб інтерфейс залишався функціональним і читабельним при ввімкненні режиму Increase Contrast. App Store Review може відхилити застосунки з критичними порушеннями контрастності.

Програмне керування фокусом доступності

У деяких сценаріях розробник повинен явно перемістити фокус Screen Reader на конкретний елемент: після відкриття модального вікна, завершення асинхронної операції або відображення повідомлення про помилку.

React Native надає два API для керування фокусом:

  1. AccessibilityInfo.setAccessibilityFocus(reactTag) — переміщує фокус Screen Reader на вказаний елемент.
  2. 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.');
  }
}

Тестування доступності з Accessibility Inspector

Apple та Google надають вбудовані інструменти для аудиту доступності під час розробки.

iOS: Accessibility Inspector (Xcode)

Активація інструменту:

  1. Відкрийте Xcode
  2. Меню: Xcode → Open Developer Tool → Accessibility Inspector
  3. Оберіть цільовий симулятор або фізичний пристрій у верхньому випадаючому меню
  4. Натисніть кнопку Inspection Pointer (іконка прицілу)

Функціонал інспектора:

Loading diagram...
flowchart TD
    A["Accessibility Inspector"] --> B["Inspection Mode"]
    A --> C["Audit"]
    A --> D["Settings"]
    
    B --> B1["Hover over element"]
    B --> B2["View accessibility properties"]
    B --> B3["Test focus order"]
    
    C --> C1["Run automated checks"]
    C --> C2["Detect missing labels"]
    C --> C3["Check contrast ratios"]
    C --> C4["Find redundant roles"]
    
    D --> D1["Simulate Dynamic Type"]
    D --> D2["Enable VoiceOver"]
    D --> D3["Test with Reduce Motion"]
    
    style A fill:#3b82f6,stroke:#1E293B,color:#ffffff
    style B fill:#DBEAFE,stroke:#1E293B,color:#1E293B
    style C fill:#DBEAFE,stroke:#1E293B,color:#1E293B
    style D fill:#DBEAFE,stroke:#1E293B,color:#1E293B

Типові проблеми, що виявляються автоматичним аудитом:

ПомилкаОписВирішення
Missing accessibility labelІнтерактивний елемент без описуДодати accessibilityLabel
Redundant accessibility labelLabel дублює текст компонентаВидалити атрибут або скоротити
Element not focusableКнопка недоступна для Screen ReaderДодати accessibilityRole="button"
Insufficient contrastКонтрастність < 4.5:1Змінити колір тексту або фону
Missing dynamic type supportТекст не масштабуєтьсяРеалізувати useScaledFontSize

Android: Accessibility Scanner

Встановлення та запуск:

  1. Встановіть Accessibility Scanner з Google Play Store на фізичному пристрої або емуляторі.
  2. Активуйте сервіс: Settings → Accessibility → Accessibility Scanner → Use service.
  3. Відкрийте ваш застосунок і натисніть плаваючу кнопку сканера.
  4. Інструмент автоматично виділить проблемні елементи та надасть рекомендації.

Приклад звіту:

❌ 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
Обмеження автоматичного тестування: Інструменти виявляють лише структурні порушення (відсутні labels, контрастність, розміри). Семантична коректність, логічність послідовності фокусу та якість формулювань потребують ручного тестування з реальним Screen Reader.

Реальне тестування з VoiceOver та TalkBack

Єдиний спосіб гарантувати доступність — це пройти весь користувацький сценарій із увімкненим Screen Reader.

Базові жести VoiceOver (iOS):

ЖестДія
Свайп вправоНаступний елемент
Свайп влівоПопередній елемент
Подвійний тапАктивувати елемент
Три пальці свайп вгору/внизПрокрутка сторінки
Z-pattern (двома пальцями)Відкрити Rotor (швидка навігація за типами)

Базові жести TalkBack (Android):

ЖестДія
Свайп вправоНаступний елемент
Свайп влівоПопередній елемент
Подвійний тапАктивувати елемент
Свайп вниз-вправо одним рухомВідкрити контекстне меню TalkBack
Два пальці свайп вгору/внизПрокрутка
Чек-лист реального тестування:
  1. ✅ Всі інтерактивні елементи мають зрозумілі labels?
  2. ✅ Порядок фокусу відповідає візуальній логіці екрану?
  3. ✅ Деструктивні дії (видалення, вихід) супроводжуються попередженнями?
  4. ✅ Помилки валідації озвучуються і фокус переміщується на проблемне поле?
  5. ✅ Модальні вікна обмежують фокус (accessibilityViewIsModal)?
  6. ✅ Декоративні елементи виключені з навігації?
  7. ✅ Текст залишається читабельним при максимальному розмірі шрифту (AX5)?
  8. ✅ Інтерфейс функціональний у режимі High Contrast?

Інтеграція i18n та a11y у реальному компоненті

Об'єднаємо отримані знання у повноцінний приклад: форма входу з підтримкою багатомовності, доступності, динамічних розмірів шрифту та коректної контрастності.

Аналіз доступності компонента


Підсумки: чеклист впровадження 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
Доступність — це неперервний процес: На відміну від функціональних вимог, які можна «закрити» одного разу, доступність потребує постійної підтримки при додаванні нових компонентів, зміні дизайну та оновленні операційних систем. Інтегруйте перевірку доступності у ваш CI/CD pipeline через автоматизовані тести (наприклад, @testing-library/react-native із правилами eslint-plugin-jsx-a11y).

Додаткові ресурси

React i18next Документація
Офіційна документація бібліотеки з прикладами інтерполяції, плюралізації та інтеграції з React Native: https://react.i18next.com/
Apple Human Interface Guidelines — Accessibility
Стандарти проєктування доступних iOS-застосунків від Apple: https://developer.apple.com/design/human-interface-guidelines/accessibility
Material Design — Accessibility
Керівництво Google з проєктування доступних Android-застосунків: https://m3.material.io/foundations/accessibility
WCAG 2.1 Guidelines
Міжнародний стандарт доступності веб-контенту (застосовний і до мобільних застосунків): https://www.w3.org/WAI/WCAG21/quickref/
React Native Accessibility API
Повна документація атрибутів доступності React Native: https://reactnative.dev/docs/accessibility
Copyright © 2026