Flutter Localization Testing: Complete Strategies Guide
Your app works perfectly in English, but crashes in Arabic. Buttons overlap in German. Dates look wrong in Japanese. These bugs slip through because most developers don't test localization properly.
Why Localization Testing Fails
Common mistakes that cause localized bugs in production:
- Testing only with English
- Not testing RTL layouts
- Ignoring text expansion (German is 30% longer than English)
- Skipping date/number formatting tests
- Not testing all plural forms
- Assuming one locale represents all languages
The Three Layers of L10n Testing
1. Unit Tests: Translation Keys
Test that all required keys exist and return proper types:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
group('Localization Keys', () {
for (final locale in AppLocalizations.supportedLocales) {
test('All keys exist for ${locale.languageCode}', () async {
final l10n = await AppLocalizations.delegate.load(locale);
// Test all keys are non-empty
expect(l10n.appTitle, isNotEmpty);
expect(l10n.welcomeMessage, isNotEmpty);
expect(l10n.loginButton, isNotEmpty);
});
}
});
group('Placeholder Replacements', () {
test('Placeholders work correctly', () async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
final message = l10n.greetingWithName('John');
expect(message, contains('John'));
expect(message, isNot(contains('{name}')));
});
});
group('Plural Forms', () {
test('All plural categories render', () async {
final l10n = await AppLocalizations.delegate.load(Locale('ar'));
// Arabic has 6 plural forms, test them all
expect(l10n.itemCount(0), isNotEmpty); // zero
expect(l10n.itemCount(1), isNotEmpty); // one
expect(l10n.itemCount(2), isNotEmpty); // two
expect(l10n.itemCount(5), isNotEmpty); // few
expect(l10n.itemCount(15), isNotEmpty); // many
expect(l10n.itemCount(100), isNotEmpty); // other
});
});
}
2. Widget Tests: Layout & Rendering
Test that UI handles different text lengths and directions:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Button Localization', () {
testWidgets('Buttons don\'t overflow in German', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: Locale('de'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: LoginScreen(),
),
);
await tester.pumpAndSettle();
// Check for render overflows
expect(tester.takeException(), isNull);
// Verify button is visible and not clipped
final button = find.byType(ElevatedButton);
expect(button, findsOneWidget);
final buttonWidget = tester.widget<ElevatedButton>(button);
final renderBox = tester.renderObject<RenderBox>(button);
expect(renderBox.hasSize, isTrue);
expect(renderBox.size.width, greaterThan(0));
});
testWidgets('RTL layout works correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: Locale('ar'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: HomeScreen(),
),
);
await tester.pumpAndSettle();
// Verify text direction
final directionality = tester.widget<Directionality>(
find.byType(Directionality).first,
);
expect(directionality.textDirection, TextDirection.rtl);
// Check leading/trailing are swapped
final appBar = tester.widget<AppBar>(find.byType(AppBar));
expect(appBar.leading, isNotNull);
});
});
}
3. Integration Tests: Full User Flows
Test complete scenarios in different locales:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Checkout Flow', () {
for (final locale in ['en', 'es', 'ar', 'de', 'ja']) {
testWidgets('Complete purchase in $locale', (tester) async {
await tester.pumpWidget(MyApp(locale: Locale(locale)));
await tester.pumpAndSettle();
// Navigate to product
await tester.tap(find.text('Shop'));
await tester.pumpAndSettle();
// Add to cart
await tester.tap(find.byIcon(Icons.add_shopping_cart).first);
await tester.pumpAndSettle();
// Go to checkout
await tester.tap(find.byIcon(Icons.shopping_cart));
await tester.pumpAndSettle();
// Verify cart total formatting
final priceText = find.byType(Text).evaluate()
.map((e) => (e.widget as Text).data)
.where((text) => text != null && text.contains(RegExp(r'[\d,.]')));
expect(priceText, isNotEmpty);
// Complete checkout
await tester.tap(find.text('Checkout'));
await tester.pumpAndSettle();
// Verify success message in correct language
expect(find.byType(SnackBar), findsOneWidget);
});
}
});
}
Golden Tests for Visual Regression
Catch visual layout issues across locales:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('Profile Card Golden Tests', () {
testGoldens('renders correctly in all locales', (tester) async {
final builder = GoldenBuilder.grid(
columns: 2,
widthToHeightRatio: 1,
)
..addScenario('English', ProfileCard(locale: Locale('en')))
..addScenario('German', ProfileCard(locale: Locale('de')))
..addScenario('Arabic', ProfileCard(locale: Locale('ar')))
..addScenario('Japanese', ProfileCard(locale: Locale('ja')));
await tester.pumpWidgetBuilder(builder.build());
await screenMatchesGolden(tester, 'profile_card_all_locales');
});
});
}
Testing Date and Number Formatting
import 'package:intl/intl.dart';
void main() {
group('Number Formatting', () {
test('Formats currency correctly per locale', () {
final amount = 1234.56;
// English (US)
expect(
NumberFormat.currency(locale: 'en_US', symbol: '\$').format(amount),
'\$1,234.56',
);
// German
expect(
NumberFormat.currency(locale: 'de_DE', symbol: '€').format(amount),
'1.234,56 €',
);
// Japanese
expect(
NumberFormat.currency(locale: 'ja_JP', symbol: '¥').format(amount),
'¥1,235', // No decimal
);
});
});
group('Date Formatting', () {
test('Formats dates correctly per locale', () {
final date = DateTime(2025, 12, 1);
expect(DateFormat.yMMMd('en_US').format(date), 'Dec 1, 2025');
expect(DateFormat.yMMMd('es_ES').format(date), '1 dic 2025');
expect(DateFormat.yMMMd('ja_JP').format(date), '2025年12月1日');
});
});
}
Automated Screenshot Testing
Generate screenshots for all locales automatically:
import 'package:integration_test/integration_test.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Screenshot Tests', () {
for (final locale in ['en', 'es', 'ar', 'de', 'ja', 'zh']) {
testWidgets('Capture $locale screenshots', (tester) async {
await tester.pumpWidget(MyApp(locale: Locale(locale)));
// Home screen
await binding.takeScreenshot('${locale}_home');
// Navigate and capture other screens
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
await binding.takeScreenshot('${locale}_settings');
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
await binding.takeScreenshot('${locale}_profile');
});
}
});
}
Run with:
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/screenshot_test.dart
Accessibility Testing
Test with screen readers in different languages:
import 'package:flutter/semantics.dart';
void main() {
testWidgets('Semantic labels are localized', (tester) async {
await tester.pumpWidget(MyApp(locale: Locale('es')));
// Enable semantics
final handle = tester.ensureSemantics();
// Check button has Spanish label
expect(
tester.getSemantics(find.byType(IconButton).first),
matchesSemantics(label: 'Cerrar'), // "Close" in Spanish
);
handle.dispose();
});
}
Testing Checklist
For every feature:
- Test in at least 3 languages (English, German for length, Arabic for RTL)
- Test all plural forms (0, 1, 2, few, many, other)
- Test date/number formatting
- Test with actual device locales, not just code
- Check text doesn't overflow
- Verify RTL layout mirror correctly
- Test keyboard input in different languages
- Check semantic labels for accessibility
- Capture screenshots for visual regression
- Test on both Android and iOS
CI/CD Integration
# .github/workflows/localization-tests.yml
name: Localization Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
locale: [en, es, de, ar, ja, zh]
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
- name: Run tests for ${{ matrix.locale }}
run: flutter test test/localization_test.dart --tags=${{ matrix.locale }}
- name: Generate golden tests
run: flutter test --update-goldens
- name: Upload screenshots
uses: actions/upload-artifact@v3
with:
name: screenshots-${{ matrix.locale }}
path: screenshots/
Common Issues to Test
- Text Overflow: German words are 30% longer
- RTL Layout: Icons and alignment flip in Arabic/Hebrew
- Date Formats: MM/DD vs DD/MM vs YYYY/MM/DD
- Number Separators: 1,234.56 vs 1.234,56
- Plurals: Test zero, one, two, few, many, other
- Character Sets: Japanese/Chinese require larger fonts
- Keyboard Types: Email keyboards differ per language
Conclusion
Localization bugs are embarrassing and expensive. They make your app feel amateur and can even cause crashes. The solution is systematic testing across all supported locales.
Don't wait for users to report localization bugs. Build comprehensive tests into your development process and catch issues before they reach production.
Automate your localization testing with FlutterLocalisation. Get automatic validation, visual testing tools, and CI/CD integration to catch localization bugs early.