Flutter Localization Debugging: Tools and Techniques to Fix Translation Issues
Localization bugs can be tricky to track down. This guide covers debugging tools, techniques, and common issues to help you find and fix translation problems quickly.
Common Localization Issues
Before diving into tools, let's identify what we're looking for:
- Missing translations - Keys that return null or fallback
- Wrong locale - App not detecting or applying correct locale
- Placeholder errors - Missing or wrong placeholder values
- Format issues - Dates, numbers, currencies displaying wrong
- RTL layout bugs - Incorrect text direction or alignment
- Build errors - ARB file syntax or generation issues
Built-in Debugging Tools
1. Flutter DevTools Locale Inspector
Flutter DevTools includes locale information:
// Enable verbose logging for localization
void main() {
debugPrint('System locale: ${WidgetsBinding.instance.platformDispatcher.locale}');
debugPrint('System locales: ${WidgetsBinding.instance.platformDispatcher.locales}');
runApp(const MyApp());
}
2. Debug Overlay for Current Locale
Create a debug overlay showing the current locale:
// lib/widgets/debug/locale_debug_overlay.dart
import 'package:flutter/material.dart';
class LocaleDebugOverlay extends StatelessWidget {
final Widget child;
final bool enabled;
const LocaleDebugOverlay({
super.key,
required this.child,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
if (!enabled) return child;
return Stack(
children: [
child,
Positioned(
top: 50,
right: 10,
child: _LocaleInfoBanner(),
),
],
);
}
}
class _LocaleInfoBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context);
final textDirection = Directionality.of(context);
return Material(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Locale: ${locale.toLanguageTag()}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Direction: ${textDirection.name}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
);
}
}
// Usage:
LocaleDebugOverlay(
enabled: kDebugMode,
child: MyApp(),
)
3. Translation Key Highlighter
Highlight missing or fallback translations:
// lib/debug/translation_highlighter.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class DebugLocalizations {
final AppLocalizations _l10n;
final bool highlightMissing;
DebugLocalizations(this._l10n, {this.highlightMissing = true});
String get(String key, String value) {
if (value.isEmpty || value == key) {
if (highlightMissing) {
return '⚠️ [$key]';
}
debugPrint('Missing translation: $key');
}
return value;
}
}
// Extension for easier access
extension DebugL10n on BuildContext {
DebugLocalizations get debugL10n {
return DebugLocalizations(
AppLocalizations.of(this)!,
highlightMissing: true,
);
}
}
VS Code Debugging
launch.json Configuration
{
"version": "0.2.0",
"configurations": [
{
"name": "Flutter (English)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"args": ["--dart-define=LOCALE=en"]
},
{
"name": "Flutter (Spanish)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"args": ["--dart-define=LOCALE=es"]
},
{
"name": "Flutter (Arabic RTL)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"args": ["--dart-define=LOCALE=ar"]
}
]
}
Use in your app:
void main() {
const forcedLocale = String.fromEnvironment('LOCALE');
runApp(MyApp(
forcedLocale: forcedLocale.isNotEmpty ? Locale(forcedLocale) : null,
));
}
VS Code Extensions
Install these helpful extensions:
- Flutter Intl - ARB file management
- ARB Editor - Syntax highlighting for ARB files
- i18n Ally - Translation management
Logging and Monitoring
Localization Logger
// lib/debug/localization_logger.dart
import 'package:flutter/foundation.dart';
class LocalizationLogger {
static bool enabled = kDebugMode;
static void logLocaleChange(Locale from, Locale to) {
if (!enabled) return;
debugPrint('🌍 Locale changed: ${from.toLanguageTag()} → ${to.toLanguageTag()}');
}
static void logTranslationAccess(String key, String? value) {
if (!enabled) return;
if (value == null || value.isEmpty) {
debugPrint('⚠️ Missing translation: $key');
} else {
debugPrint('📝 Translation: $key = "$value"');
}
}
static void logPlaceholderError(String key, String placeholder, dynamic value) {
if (!enabled) return;
debugPrint('❌ Placeholder error in "$key": {$placeholder} received ${value.runtimeType}');
}
static void logFormatError(String key, String format, dynamic value, Object error) {
if (!enabled) return;
debugPrint('❌ Format error in "$key": format=$format, value=$value, error=$error');
}
}
Wrapper for Safe Translation Access
// lib/l10n/safe_localizations.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../debug/localization_logger.dart';
class SafeLocalizations {
final AppLocalizations _l10n;
final String _currentLocale;
SafeLocalizations(BuildContext context)
: _l10n = AppLocalizations.of(context)!,
_currentLocale = Localizations.localeOf(context).toLanguageTag();
String get appTitle {
final value = _l10n.appTitle;
LocalizationLogger.logTranslationAccess('appTitle', value);
return value;
}
String itemCount(int count) {
try {
final value = _l10n.itemCount(count);
LocalizationLogger.logTranslationAccess('itemCount($count)', value);
return value;
} catch (e) {
LocalizationLogger.logPlaceholderError('itemCount', 'count', count);
return '$count items';
}
}
// Add other translations with logging...
}
extension SafeL10n on BuildContext {
SafeLocalizations get safeL10n => SafeLocalizations(this);
}
ARB File Validation
Validation Script
// tool/validate_arb.dart
import 'dart:convert';
import 'dart:io';
void main() async {
final arbDir = Directory('lib/l10n');
final files = arbDir.listSync().whereType<File>().where(
(f) => f.path.endsWith('.arb'),
);
final errors = <String>[];
final templateFile = files.firstWhere(
(f) => f.path.contains('app_en.arb'),
);
final templateContent = jsonDecode(await templateFile.readAsString());
final templateKeys = templateContent.keys
.where((k) => !k.toString().startsWith('@'))
.toSet();
for (final file in files) {
final content = await file.readAsString();
// Check JSON validity
try {
final json = jsonDecode(content) as Map<String, dynamic>;
// Check for missing keys
final keys = json.keys
.where((k) => !k.startsWith('@'))
.toSet();
final missingKeys = templateKeys.difference(keys);
for (final key in missingKeys) {
errors.add('${file.path}: Missing key "$key"');
}
// Check placeholders
for (final entry in json.entries) {
if (entry.key.startsWith('@')) continue;
final value = entry.value.toString();
final metadata = json['@${entry.key}'];
// Find placeholders in value
final placeholderRegex = RegExp(r'\{(\w+)\}');
final foundPlaceholders = placeholderRegex
.allMatches(value)
.map((m) => m.group(1)!)
.toSet();
// Check against metadata
if (metadata != null && metadata['placeholders'] != null) {
final declaredPlaceholders =
(metadata['placeholders'] as Map).keys.toSet();
final undeclared = foundPlaceholders.difference(
declaredPlaceholders.cast<String>(),
);
for (final p in undeclared) {
errors.add('${file.path}: Undeclared placeholder {$p} in "${entry.key}"');
}
} else if (foundPlaceholders.isNotEmpty) {
errors.add(
'${file.path}: "${entry.key}" has placeholders but no @metadata',
);
}
}
} catch (e) {
errors.add('${file.path}: Invalid JSON - $e');
}
}
if (errors.isEmpty) {
print('✅ All ARB files are valid');
} else {
print('❌ Found ${errors.length} errors:');
for (final error in errors) {
print(' - $error');
}
exit(1);
}
}
Run with:
dart run tool/validate_arb.dart
Testing for Localization Issues
Unit Tests for Translations
// test/l10n/translations_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'dart:convert';
import 'dart:io';
void main() {
group('ARB Files', () {
late Map<String, dynamic> englishArb;
late List<File> arbFiles;
setUpAll(() async {
final arbDir = Directory('lib/l10n');
arbFiles = arbDir
.listSync()
.whereType<File>()
.where((f) => f.path.endsWith('.arb'))
.toList();
final englishFile = arbFiles.firstWhere(
(f) => f.path.contains('app_en.arb'),
);
englishArb = jsonDecode(await englishFile.readAsString());
});
test('all ARB files are valid JSON', () async {
for (final file in arbFiles) {
expect(
() => jsonDecode(file.readAsStringSync()),
returnsNormally,
reason: '${file.path} should be valid JSON',
);
}
});
test('all ARB files have required keys', () async {
final requiredKeys = englishArb.keys
.where((k) => !k.startsWith('@') && !k.startsWith('@@'))
.toList();
for (final file in arbFiles) {
final content = jsonDecode(await file.readAsString());
for (final key in requiredKeys) {
expect(
content.containsKey(key),
true,
reason: '${file.path} should have key "$key"',
);
}
}
});
test('no empty translations', () async {
for (final file in arbFiles) {
final content = jsonDecode(await file.readAsString()) as Map;
for (final entry in content.entries) {
if (entry.key.toString().startsWith('@')) continue;
expect(
entry.value.toString().trim().isNotEmpty,
true,
reason: '${file.path}: "${entry.key}" should not be empty',
);
}
}
});
});
}
Widget Tests with Different Locales
// test/widgets/localized_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
Widget createTestWidget(Locale locale, Widget child) {
return MaterialApp(
locale: locale,
supportedLocales: const [
Locale('en'),
Locale('es'),
Locale('ar'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: child,
);
}
group('Localized Widget Tests', () {
testWidgets('displays English text', (tester) async {
await tester.pumpWidget(createTestWidget(
const Locale('en'),
const MyWidget(),
));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
});
testWidgets('displays Spanish text', (tester) async {
await tester.pumpWidget(createTestWidget(
const Locale('es'),
const MyWidget(),
));
await tester.pumpAndSettle();
expect(find.text('Bienvenido'), findsOneWidget);
});
testWidgets('handles RTL for Arabic', (tester) async {
await tester.pumpWidget(createTestWidget(
const Locale('ar'),
const MyWidget(),
));
await tester.pumpAndSettle();
final directionality = tester.widget<Directionality>(
find.byType(Directionality).first,
);
expect(directionality.textDirection, TextDirection.rtl);
});
});
}
Debug Panel Widget
Create an in-app debug panel for localization:
// lib/widgets/debug/localization_debug_panel.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizationDebugPanel extends StatelessWidget {
const LocalizationDebugPanel({super.key});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context);
final direction = Directionality.of(context);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: const Text(
'Localization Debug',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
_InfoTile(
title: 'Current Locale',
value: locale.toLanguageTag(),
),
_InfoTile(
title: 'Language Code',
value: locale.languageCode,
),
_InfoTile(
title: 'Country Code',
value: locale.countryCode ?? 'Not set',
),
_InfoTile(
title: 'Text Direction',
value: direction.name.toUpperCase(),
),
const Divider(),
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Quick Locale Switch',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
_LocaleSwitchTile(locale: const Locale('en'), label: 'English'),
_LocaleSwitchTile(locale: const Locale('es'), label: 'Spanish'),
_LocaleSwitchTile(locale: const Locale('ar'), label: 'Arabic (RTL)'),
_LocaleSwitchTile(locale: const Locale('ja'), label: 'Japanese'),
const Divider(),
ListTile(
leading: const Icon(Icons.bug_report),
title: const Text('Show Translation Keys'),
trailing: Switch(
value: false, // Connect to your debug state
onChanged: (value) {
// Toggle showing raw keys
},
),
),
],
),
);
}
}
class _InfoTile extends StatelessWidget {
final String title;
final String value;
const _InfoTile({required this.title, required this.value});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
trailing: Text(
value,
style: const TextStyle(fontFamily: 'monospace'),
),
);
}
}
class _LocaleSwitchTile extends StatelessWidget {
final Locale locale;
final String label;
const _LocaleSwitchTile({required this.locale, required this.label});
@override
Widget build(BuildContext context) {
final currentLocale = Localizations.localeOf(context);
final isSelected = currentLocale.languageCode == locale.languageCode;
return ListTile(
leading: Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? Colors.green : null,
),
title: Text(label),
subtitle: Text(locale.toLanguageTag()),
onTap: () {
// Switch locale using your provider
// context.read<LocaleProvider>().setLocale(locale);
},
);
}
}
Common Issues and Solutions
Issue 1: Translations Not Updating
Problem: Changed ARB file but app shows old text.
Solution:
# Clean and regenerate
flutter clean
flutter pub get
flutter gen-l10n
flutter run
Issue 2: Null Returned for Translation
Problem: AppLocalizations.of(context) returns null.
Solution: Check your MaterialApp setup:
MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate, // Must be present!
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
)
Issue 3: Placeholder Type Mismatch
Problem: Runtime error with placeholders.
Solution: Verify types match:
// ARB
"greeting": "Hello {name}",
"@greeting": {
"placeholders": {
"name": {"type": "String"} // Must match Dart call
}
}
// Dart - correct
l10n.greeting('John')
// Dart - wrong (passing int)
l10n.greeting(123)
Issue 4: Wrong Plural Form
Problem: Plural not working correctly.
Solution: Check ICU syntax:
// Correct
"items": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
// Wrong (missing 'other')
"items": "{count, plural, =0{No items} =1{1 item}}"
Conclusion
Effective localization debugging requires:
- Good logging - Know what's happening
- Visual indicators - See issues immediately
- Validation scripts - Catch issues early
- Comprehensive tests - Prevent regressions
Build these tools into your workflow and you'll catch translation bugs before they reach users.
Need help managing your translations? FlutterLocalisation provides validation, team collaboration, and ensures your ARB files stay consistent across all languages.