← Back to Blog

Flutter Localization Testing: Complete Strategies Guide

flutterlocalizationtestingquality-assuranceautomation

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

  1. Text Overflow: German words are 30% longer
  2. RTL Layout: Icons and alignment flip in Arabic/Hebrew
  3. Date Formats: MM/DD vs DD/MM vs YYYY/MM/DD
  4. Number Separators: 1,234.56 vs 1.234,56
  5. Plurals: Test zero, one, two, few, many, other
  6. Character Sets: Japanese/Chinese require larger fonts
  7. 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.