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
dependencies:
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: truearb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false{
"@@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" }
}
}
}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
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.
// 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// 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.
// 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.
// 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
intlwith the active locale. - Use
EdgeInsetsDirectionalandAlignmentDirectionalso 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.