Skip to content
FlutterLearn

Intermediate · Lesson 10 · 12 min read

Internationalization and Accessibility

Translate your app properly, handle plurals, dates and right-to-left layouts, and make every screen usable with a screen reader.

Updated August 1, 2026

What you will learn

  • Set up ARB-based localisation with generated type-safe strings
  • Handle plurals, dates, numbers and currency correctly
  • Support right-to-left languages without hard-coding directions
  • Make screens work with screen readers and large text

These two topics belong together: both are about not assuming your user is you. Both are far cheaper to build in than to retrofit, and both are legal requirements in a growing number of markets.

Setting up localisation

pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: any

flutter:
  generate: true
l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
lib/l10n/app_en.arb
{
  "@@locale": "en",

  "appTitle": "FlutterLearn",
  "@appTitle": {
    "description": "The application name shown in the app bar"
  },

  "welcomeUser": "Welcome back, {name}",
  "@welcomeUser": {
    "description": "Greeting on the home screen",
    "placeholders": { "name": { "type": "String" } }
  },

  "lessonCount": "{count, plural, =0{No lessons} =1{1 lesson} other{{count} lessons}}",
  "@lessonCount": {
    "placeholders": { "count": { "type": "int" } }
  },

  "lastUpdated": "Updated {date}",
  "@lastUpdated": {
    "placeholders": {
      "date": { "type": "DateTime", "format": "yMMMd" }
    }
  }
}
Dart
MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  // Omit 'locale' to follow the device setting
  home: const HomeScreen(),
);

// In any widget — generated, so typos are compile errors
final l10n = AppLocalizations.of(context);

Text(l10n.appTitle);
Text(l10n.welcomeUser(user.firstName));
Text(l10n.lessonCount(lessons.length));
Text(l10n.lastUpdated(lesson.updatedAt));

Dates, numbers and currency

Dart
import 'package:intl/intl.dart';

final locale = Localizations.localeOf(context).toString();

DateFormat.yMMMd(locale).format(date);        // Aug 1, 2026 / 1 août 2026
DateFormat.jm(locale).format(date);           // 2:30 PM / 14:30

NumberFormat.decimalPattern(locale).format(1234567.89);
NumberFormat.percentPattern(locale).format(0.87);
NumberFormat.currency(locale: locale, symbol: '€').format(49.99);

// Relative time reads better than a raw date for recent items
final difference = DateTime.now().difference(date);
final label = difference.inDays == 0
    ? l10n.today
    : DateFormat.yMMMd(locale).format(date);

Formatting numbers by hand with string interpolation produces 1234567.89 in every locale — wrong for the large share of the world that uses 1.234.567,89.

Right-to-left layouts

Arabic, Hebrew, Persian and Urdu run right to left. Flutter mirrors most layouts automatically — but only if you use direction-aware values instead of literal left and right.

Dart
// Wrong: stays on the physical left even in Arabic
padding: const EdgeInsets.only(left: 16),
alignment: Alignment.centerLeft,

// Right: flips automatically with the text direction
padding: const EdgeInsetsDirectional.only(start: 16),
alignment: AlignmentDirectional.centerStart,

// Same for borders and radii
borderRadius: const BorderRadiusDirectional.only(
  topStart: Radius.circular(12),
),

// Icons that imply direction should mirror too
Icon(Icons.arrow_back)                        // does not mirror
Icon(Icons.arrow_back_ios_new, textDirection: Directionality.of(context))
// Or use the auto-mirroring variants where they exist
Dart
// Preview RTL without changing your device language
Directionality(
  textDirection: TextDirection.rtl,
  child: const MyScreen(),
)

Screen reader support

Flutter builds a semantics tree alongside the widget tree, which TalkBack and VoiceOver read. Most standard widgets populate it correctly; the gaps are in custom UI.

Dart
// An icon-only button is silent without a label
IconButton(
  icon: const Icon(Icons.bookmark_border),
  tooltip: l10n.saveLesson,     // both a tooltip and a semantic label
  onPressed: _save,
)

// Describe a custom control's purpose, state and value
Semantics(
  label: l10n.lessonProgress,
  value: '${(progress * 100).round()}%',
  child: CustomProgressRing(progress: progress),
)

// Decorative images should be skipped, not announced as "image"
ExcludeSemantics(
  child: Image.asset('assets/images/pattern.png'),
)

// Merge a row into one announcement instead of three fragments
MergeSemantics(
  child: Row(
    children: [
      const Icon(Icons.schedule),
      const SizedBox(width: 8),
      Text(l10n.readingTime(lesson.minutes)),
    ],
  ),
)

// Announce something that changed without a visual focus move
SemanticsService.announce(l10n.itemDeleted, TextDirection.ltr);

The rest of accessibility

  • Contrast. Body text needs 4.5:1 against its background, large text 3:1. Check both light and dark themes — a colour that passes on white often fails on dark grey.
  • Touch targets of at least 48×48 logical pixels, with spacing between them.
  • Text scaling. Users can set 200%+. Avoid fixed-height containers around text and test at large scales.
  • Do not rely on colour alone. A red border for an invalid field is invisible to colour-blind users — pair it with an icon and a message.
  • Respect reduced motion via MediaQuery.disableAnimationsOf(context).
  • Keyboard and focus order on web and desktop: everything interactive must be reachable by Tab, in a sensible order, with a visible focus indicator.
Dart
// Guideline-based automated checks in a widget test
testWidgets('lesson screen meets accessibility guidelines', (tester) async {
  final handle = tester.ensureSemantics();
  await tester.pumpWidget(const MaterialApp(home: LessonScreen()));

  await expectLater(tester, meetsGuideline(textContrastGuideline));
  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
  await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
  await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));

  handle.dispose();
});

Key takeaways

  • Use ARB files with ICU plural syntax; never build translated sentences by concatenation.
  • Format dates, numbers and currency through intl with the active locale.
  • Use EdgeInsetsDirectional and AlignmentDirectional so layouts mirror in RTL.
  • Label icon-only controls, merge related text, and test with a real screen reader.

Practice

Localise and audit one screen

Take a screen with hard-coded English strings and localise it into two languages, one of them right-to-left. Use a plural and a formatted date. Then run the four accessibility guideline tests, fix what fails, and complete the flow using TalkBack or VoiceOver.

Show hints
  • Pseudo-localisation (very long fake strings) exposes layouts that break on translation — German is often 30% longer than English.
  • Missing keys in a non-template ARB file fall back to the template; check the generated file to confirm what was translated.