Irbisa · Шпаргалка
Доступность и локализация
Semantics, скринридеры, масштаб текста, flutter_localizations, ARB, intl, плюралы, RTL
Что делает виджет
Semanticsи чем от него отличаютсяMergeSemanticsиExcludeSemantics?// Semantics: say what a custom widget meansSemantics( label: 'Rating', value: '$stars of 5', child: ExcludeSemantics(child: StarRow(stars: stars)), // 5 icons -> 1 node);…TalkBack озвучивает иконку в app bar просто как «кнопка». Как дать имя элементу управления из одной иконки?
// wrong: announced as "button" and nothing elseIconButton(icon: const Icon(Icons.search), onPressed: _openSearch); // right: the tooltip is read as the name; sighted users see it on long-pressIconButton( icon: const Icon(Icons.search),…Как локализовать Flutter-приложение: за что отвечают
flutter_localizations, ARB-файлы,gen-l10nиintl?// l10n.yaml: arb-dir: lib/l10n// template-arb-file: app_en.arb// lib/l10n/app_en.arb:// { "cartTitle": "Cart",// "@cartTitle": { "description": "Title of the cart screen" } }// lib/l10n/app_ru.arb: { "cartTitle": "Корзина" }…Как сделать Flutter-приложение доступным: что вы проверяете на каждом экране?
Что TalkBack озвучит для этой строки и что изменится, если удалить
MergeSemantics?bool agreed = false;MergeSemantics( child: Row( children: [ Checkbox( value: agreed, onChanged: (v) => setState(() => agreed = v ?? false), ), const Text('I agree to the terms'), ], ),)Карточку товара с названием, ценой и зоной нажатия читают как три отдельные остановки. Как сделать её одной кнопкой?
// wrong: three stops - "Name", "5 EUR" and an unnamed tap regionStack(children: [ Column(children: [Text(name), Text('$price EUR')]), Positioned.fill(child: InkWell(onTap: _open)),]);…Найдите баг в этом самодельном переключателе: он выглядит правильно и переключается по нажатию, но пользователь TalkBack не понимает, включён ли он.
Semantics( button: true, label: label, child: GestureDetector( onTap: () => onChanged(!value), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 64, height: 48, alignment: value ? Alignment.centerRight : Alignment.centerLeft, color: value ? Colors.green : Colors.grey, child: const CircleAvatar(radius: 20, backgroundColor: Colors.white), ), ),)Фильтр меняет число результатов, но скринридер молчит. Как объявить изменение, на котором нет фокуса?
// right: status text that is re-read whenever it changesSemantics( liveRegion: true, child: Text('$count results'),);…Tab прыгает по форме в бессмысленном порядке, хотя выглядит она нормально. Что задаёт этот порядок?
// wrong: two visual columns; traversal zigzags left-right-left-rightRow(children: [ Column(children: [TextField(controller: firstName), TextField(controller: lastName)]), Column(children: [TextField(controller: street), TextField(controller: city)]),]);…Аудит ругает ваш ряд кастомных чипов за размер зоны нажатия и контраст, а соседние кнопки Material пропускает. Что делает Material, чего не делаете вы?
// wrong: 24 dp target, padding outside the detector, ~2:1 contrastPadding( padding: const EdgeInsets.all(12), child: GestureDetector( onTap: _remove, child: const Icon(Icons.close, color: Color(0xFFBDBDBD)),…Пользователь включил «Уменьшение движения», а онбординг всё равно делает параллакс и листается сам. Какие флаги есть в
MediaQueryи что должно поменяться?Эта подпись работает на английском. Что она покажет по-русски для 1, 2, 5 и 21 товара и как это исправить?
String itemsLabel(int n, String lang) { if (lang == 'ru') { return '$n ${n == 1 ? 'товар' : 'товаров'}'; } return '$n ${n == 1 ? 'item' : 'items'}';}Немецкий пользователь видит цену как $1,234.50, дату как 12/03/2026, а событие показывается не в тот час. Что не так с кодом форматирования?
import 'package:timezone/data/latest.dart' as tzdata;import 'package:timezone/timezone.dart' as tz; // wrong: pattern frozen to one country, no locale, the handset's zoneText('${DateFormat('dd/MM/yyyy HH:mm').format(start.toLocal())} ' '${NumberFormat('\$#,##0.00').format(price)}');…Приложение запускается на английском на испанском телефоне, хотя
app_es.arbесть. Как Flutter выбирает локаль и как добавить переключатель языка в приложении?final localeStore = ValueNotifier<Locale?>(null); // null = follow the system Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final prefs = await SharedPreferences.getInstance(); final code = prefs.getString('locale');…Найдите баг: после выбора немецкого во встроенном переключателе языка все экраны меняют язык, кроме этого, пока его не открыть заново.
class _OrdersPageState extends State<OrdersPage> { late final l10n = AppLocalizations.of(context)!; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(l10n.ordersTitle)), body: widget.orders.isEmpty ? Center(child: Text(l10n.noOrders)) : OrderList(orders: widget.orders), ); }}В арабской версии иконка в строке остаётся слева, а отступ оказывается не с той стороны. Что Flutter отражает сам, а что не отразит никогда?
// wrong: physical sides - identical in Arabic, so wrong in ArabicPadding( padding: const EdgeInsets.only(left: 16, right: 8), child: Align(alignment: Alignment.centerLeft, child: Text(name)),);…Ваши виджет-тесты проверяют
find.text('Save'), а немецкий экран всё равно уехал в релиз без перевода. Как тестировать локализованный UI?Widget wrap(Widget child, Locale locale) => MaterialApp( locale: locale, // pins this test's language localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: child, );…Бэкенд возвращает
{"code": "INSUFFICIENT_FUNDS"}, а ваш Bloc хранит готовую фразу для показа. Что сломается и где должен происходить перевод?sealed class Failure { const Failure(); }class InsufficientFunds extends Failure { const InsufficientFunds(this.shortfall); final num shortfall; }class SessionExpired extends Failure { const SessionExpired(); }class UnknownFailure extends Failure { const UnknownFailure(this.code); final String code; } // Data layer: wire code -> typed failure, no display text…PR добавляет тулбар из одних иконок. Что о его доступности может доказать виджет-тест, а для чего всё ещё нужно устройство с TalkBack?
testWidgets('editor toolbar meets the accessibility guidelines', (tester) async { await tester.pumpWidget(const MaterialApp(home: EditorPage())); await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); // 48 dp await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); // 44 pt await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));…TalkBack полностью пропускает ваш график выручки. Как дать
CustomPainterили кастомномуRenderObjectнастоящую семантику?class BarsPainter extends CustomPainter { BarsPainter(this.values, this.labels); final List<double> values; final List<String> labels; @override…Вы растите приложение с одного языка до многих. Что нужно положить в CI и чем помогает псевдолокализация?