Irbisa · Cheatsheet

Accessibility & Localization

Semantics, screen readers, text scaling, flutter_localizations, ARB, intl, plurals, RTL

Middle Developer · 21 questions

  1. 01

    What does the Semantics widget do, and how do MergeSemantics and ExcludeSemantics differ from it?

    Semantics describes what a piece of UI means to assistive technology: a label, a role such as button or header, a value and the actions it supports.

    // Semantics: say what a custom widget meansSemantics(  label: 'Rating',  value: '$stars of 5',  child: ExcludeSemantics(child: StarRow(stars: stars)), // 5 icons -> 1 node);…
  2. 02

    TalkBack announces your app-bar icon as just "button". How do you give an icon-only control a name?

    Give it an accessible name: IconButton(tooltip: 'Search products') sends the tooltip to the screen reader as the button's name, and a bare Icon takes semanticLabel.

    // 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),…
  3. 03

    How do you localize a Flutter app: what do flutter_localizations, ARB files, gen-l10n and intl each do?

    You write the strings into ARB files, flutter gen-l10n turns them into a typed AppLocalizations class, flutter_localizations supplies the translations of Material and Cupertino's own texts, and intl formats plurals, dates and numbers.

    // 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": "Корзина" }…
  4. 04

    How do you make a Flutter app accessible: what do you check on every screen?

    Material widgets give you most of it; your work is the custom parts: every control has a name and a role, targets are 48 dp, contrast passes, the layout survives 200% text, and the reading order makes sense with a screen reader on.

  5. 05

    What does TalkBack announce for this row, and what changes if you delete MergeSemantics?

    bool agreed = false;MergeSemantics(  child: Row(    children: [      Checkbox(        value: agreed,        onChanged: (v) => setState(() => agreed = v ?? false),      ),      const Text('I agree to the terms'),    ],  ),)

    One focus stop that reads the label, role and state together, roughly "I agree to the terms, check box, not checked" (order and wording vary by TalkBack version and language).

  6. 06

    A product card with a title, a price and a tap target is read as three separate stops. How do you make it one button?

    Give the card one semantics node of its own with a role, a hand-written label and the tap action, and drop the children's nodes: Semantics(container: true, button: true, label: ..., onTap: ..., excludeSemantics: true, child: card).

    // 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)),]);…
  7. 07

    Find the bug in this custom on/off switch: it looks right and toggles on tap, yet a TalkBack user cannot tell whether it is on.

    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),    ),  ),)

    The node has a name and a button role but no state: on and off are shown only by colour and the knob's position, so TalkBack reads the label and "button" either way, and after a double tap the user hears nothing that changed.

  8. 08

    A filter changes the result count but the screen reader stays silent. How do you announce a change nobody focused?

    Mark the text that changes as a live region: Semantics(liveRegion: true, child: Text('$n results')).

    // right: status text that is re-read whenever it changesSemantics(  liveRegion: true,  child: Text('$count results'),);…
  9. 09

    Tab jumps around your form in an order that makes no sense, though it looks fine. What decides that order?

    Geometry decides it, not the order you wrote the widgets in: the default ReadingOrderTraversalPolicy sorts focusable widgets by position under the current Directionality, row by row.

    // 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)]),]);…
  10. 10

    An audit flags your custom chip row for tap-target size and contrast but passes the Material buttons beside it. What is Material doing that you are not?

    Material pads every control's hit area to 48x48 dp however small it looks, and takes colours from a ColorScheme whose pairs are designed to contrast.

    // 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)),…
  11. 11

    A user turns on Reduce Motion and your onboarding still parallaxes and auto-advances. Which MediaQuery flags exist, and what should change?

    Flutter exposes the system accessibility switches on MediaQuery but honours few itself (Text bolds, a default AnimationController runs at 5% duration).

  12. 12

    This label works in English. What does it show in Russian for 1, 2, 5 and 21 items, and how do you fix it?

    String itemsLabel(int n, String lang) {  if (lang == 'ru') {    return '$n ${n == 1 ? 'товар' : 'товаров'}';  }  return '$n ${n == 1 ? 'item' : 'items'}';}

    It shows "1 товар", "2 товаров", "5 товаров" and "21 товаров": two of four are wrong, because Russian picks among three forms by the last digits, not by "one or more".

  13. 13

    A German user sees a price as $1,234.50 and a date as 12/03/2026, and an event shows the wrong hour. What did the formatting code get wrong?

    Two shortcuts: formatting without a locale, and treating the phone's time zone as the event's.

    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)}');…
  14. 14

    The app comes up in English on a Spanish phone even though app_es.arb exists. How does Flutter pick a locale, and how do you add an in-app language switch?

    Flutter matches the user's ordered list of preferred locales against supportedLocales and falls back to the first supported one.

    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');…
  15. 15

    Find the bug: after the user picks German in the in-app language switch, every screen changes language except this one, until it is reopened.

    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),    );  }}

    The late final field reads AppLocalizations once, on the first build, and keeps that English instance for the life of the State.

  16. 16

    In Arabic your row's icon stays on the left and the padding sits on the wrong side. What mirrors automatically in Flutter and what never does?

    Everything that reads the ambient Directionality mirrors itself; everything that names a physical side never does.

    // 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)),);…
  17. 17

    Your widget tests assert on find.text('Save'), yet a German screen shipped untranslated. How should localized UI be tested?

    Pump the widget under a real Localizations scope with an explicit locale, and assert against the same generated strings the UI uses.

    Widget wrap(Widget child, Locale locale) => MaterialApp(      locale: locale, // pins this test's language      localizationsDelegates: AppLocalizations.localizationsDelegates,      supportedLocales: AppLocalizations.supportedLocales,      home: child,    );…
  18. 18

    The backend returns {"code": "INSUFFICIENT_FUNDS"} and your Bloc stores the sentence it will show. What breaks, and where does translation belong?

    Translate at the presentation edge: the data layer turns the wire code into a typed failure, the widget turns that failure into text with AppLocalizations, and nothing in between holds display strings.

    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…
  19. 19

    A PR adds an icon-only toolbar. What can a widget test prove about its accessibility, and what still needs a device with TalkBack on?

    A widget test proves the mechanical rules through meetsGuideline: tap-target size, text contrast and a label on every tappable node.

    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));…
  20. 20

    TalkBack skips your revenue chart completely. How do you give a CustomPainter or a custom RenderObject real semantics?

    A canvas produces pixels and the accessibility bridge only sees semantics nodes, so a CustomPaint is silent unless you describe it: one summary Semantics label around it, or per-region nodes from CustomPainter.semanticsBuilder.

    class BarsPainter extends CustomPainter {  BarsPainter(this.values, this.labels);  final List<double> values;  final List<String> labels;   @override…
  21. 21

    You are growing an app from one language to many. What do you put in CI, and how does pseudo-localization help?

    One English ARB is the source of truth, CI syncs it with the translation platform, the build fails on missing translations, and pseudo-localization finds layout damage before any translator does.