Localization¶
Introduction¶
Localization in Bond provides comprehensive internationalization (i18n) support for building apps that work seamlessly across different languages, regions, and cultures. Bond's localization system integrates with Flutter's built-in l10n tooling while adding conventions and helpers for common localization scenarios.
Why Structured Localization¶
Traditional Problems¶
Flutter localization often involves scattered string management:
// Traditional approach - hardcoded strings
Text('Welcome to our app')
Text('Please enter your email address')
Text('${user.name} has ${posts.length} posts')
Problems: 1. Hardcoded strings throughout the codebase 2. No centralized translation management 3. Difficult pluralization and formatting 4. Poor RTL support without proper testing 5. Inconsistent terminology across screens
Bond's Solution¶
// Bond approach - centralized and type-safe
Text(context.l10n.welcomeMessage)
Text(context.l10n.emailInputHint)
Text(context.l10n.userPostCount(user.name, posts.length))
Setup¶
Dependencies¶
Add localization dependencies to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: any
dev_dependencies:
flutter_gen: ^5.3.2
flutter:
generate: true
Configuration¶
Create l10n.yaml:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
ARB Files¶
Create ARB files for each supported language:
lib/l10n/app_en.arb:
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The title of the application"
},
"welcomeMessage": "Welcome to {appName}",
"@welcomeMessage": {
"description": "Welcome message shown on home screen",
"placeholders": {
"appName": {
"type": "String",
"example": "My App"
}
}
},
"userPostCount": "{userName} has {count, plural, =0{no posts} =1{1 post} other{{count} posts}}",
"@userPostCount": {
"description": "Shows how many posts a user has",
"placeholders": {
"userName": {
"type": "String",
"example": "John"
},
"count": {
"type": "int",
"example": 5
}
}
},
"loginButton": "Log In",
"logoutButton": "Log Out",
"emailLabel": "Email Address",
"passwordLabel": "Password",
"forgotPasswordLink": "Forgot your password?",
"createAccountLink": "Create an account",
"loadingMessage": "Loading...",
"errorGeneric": "Something went wrong. Please try again.",
"errorNetwork": "Network error. Please check your connection.",
"errorValidationEmail": "Please enter a valid email address",
"errorValidationPasswordTooShort": "Password must be at least {minLength} characters",
"@errorValidationPasswordTooShort": {
"placeholders": {
"minLength": {
"type": "int"
}
}
}
}
lib/l10n/app_ar.arb:
{
"@@locale": "ar",
"appTitle": "تطبيقي",
"welcomeMessage": "مرحباً بك في {appName}",
"userPostCount": "{userName} لديه {count, plural, =0{لا توجد منشورات} =1{منشور واحد} other{{count} منشورات}}",
"loginButton": "تسجيل الدخول",
"logoutButton": "تسجيل الخروج",
"emailLabel": "عنوان البريد الإلكتروني",
"passwordLabel": "كلمة المرور",
"forgotPasswordLink": "نسيت كلمة المرور؟",
"createAccountLink": "إنشاء حساب",
"loadingMessage": "جاري التحميل...",
"errorGeneric": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
"errorNetwork": "خطأ في الشبكة. يرجى التحقق من الاتصال.",
"errorValidationEmail": "يرجى إدخال عنوان بريد إلكتروني صحيح",
"errorValidationPasswordTooShort": "يجب أن تكون كلمة المرور {minLength} أحرف على الأقل"
}
App Integration¶
Material App Setup¶
// lib/app/app.dart
class BondApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bond App',
// Localization setup
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// Theme setup
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
// Navigation setup
navigatorKey: AppRouter.navigatorKey,
onGenerateRoute: RouteGenerator.generateRoute,
home: HomePage(),
);
}
}
Context Extension¶
Create convenient access to localizations:
// lib/core/extensions/localization_extensions.dart
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
bool get isRTL => Directionality.of(this) == TextDirection.rtl;
bool get isLTR => !isRTL;
Locale get locale => Localizations.localeOf(this);
String get languageCode => locale.languageCode;
}
Usage in Widgets¶
Basic Usage¶
class WelcomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.appTitle),
),
body: Padding(
padding: context.pageInsets,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.welcomeMessage(context.l10n.appTitle),
style: context.textStyles.headlineLarge,
),
SizedBox(height: AppSpacing.lg),
ElevatedButton(
onPressed: () => AppRouter.pushLogin(),
child: Text(context.l10n.loginButton),
),
TextButton(
onPressed: () => AppRouter.pushRegister(),
child: Text(context.l10n.createAccountLink),
),
],
),
),
);
}
}
Form Localization¶
class LoginForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: context.l10n.emailLabel,
hintText: context.l10n.emailInputHint,
),
validator: (value) {
if (value?.isEmpty ?? true) {
return context.l10n.errorValidationRequired(context.l10n.emailLabel);
}
if (!EmailValidator.validate(value!)) {
return context.l10n.errorValidationEmail;
}
return null;
},
),
SizedBox(height: AppSpacing.md),
TextFormField(
decoration: InputDecoration(
labelText: context.l10n.passwordLabel,
),
obscureText: true,
validator: (value) {
if (value?.isEmpty ?? true) {
return context.l10n.errorValidationRequired(context.l10n.passwordLabel);
}
if (value!.length < 8) {
return context.l10n.errorValidationPasswordTooShort(8);
}
return null;
},
),
SizedBox(height: AppSpacing.lg),
ElevatedButton(
onPressed: _onSubmit,
child: Text(context.l10n.loginButton),
),
TextButton(
onPressed: () => AppRouter.pushForgotPassword(),
child: Text(context.l10n.forgotPasswordLink),
),
],
);
}
}
Advanced Features¶
Pluralization¶
Handle complex pluralization rules:
{
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"timeAgo": "{minutes, plural, =0{Just now} =1{1 minute ago} other{{minutes} minutes ago}}",
"fileSize": "{bytes, plural, =0{Empty} =1{1 byte} other{{bytes} bytes}}"
}
Date and Number Formatting¶
// lib/core/utils/format_utils.dart
class FormatUtils {
static String formatDate(BuildContext context, DateTime date) {
final formatter = DateFormat.yMMMd(context.languageCode);
return formatter.format(date);
}
static String formatTime(BuildContext context, DateTime time) {
final formatter = DateFormat.jm(context.languageCode);
return formatter.format(time);
}
static String formatNumber(BuildContext context, num number) {
final formatter = NumberFormat.decimalPattern(context.languageCode);
return formatter.format(number);
}
static String formatCurrency(BuildContext context, double amount, String currencyCode) {
final formatter = NumberFormat.currency(
locale: context.languageCode,
symbol: currencyCode,
);
return formatter.format(amount);
}
}
RTL Support¶
// lib/core/widgets/rtl_aware_widget.dart
class RTLAwareRow extends StatelessWidget {
final List<Widget> children;
final MainAxisAlignment mainAxisAlignment;
const RTLAwareRow({
Key? key,
required this.children,
this.mainAxisAlignment = MainAxisAlignment.start,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: mainAxisAlignment,
textDirection: context.isRTL ? TextDirection.rtl : TextDirection.ltr,
children: context.isRTL ? children.reversed.toList() : children,
);
}
}
class RTLAwarePadding extends StatelessWidget {
final Widget child;
final double? start;
final double? end;
final double? top;
final double? bottom;
const RTLAwarePadding({
Key? key,
required this.child,
this.start,
this.end,
this.top,
this.bottom,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsetsDirectional.only(
start: start ?? 0,
end: end ?? 0,
top: top ?? 0,
bottom: bottom ?? 0,
),
child: child,
);
}
}
Language Switching¶
Language Service¶
// lib/core/services/language_service.dart
class LanguageService extends ChangeNotifier {
Locale _currentLocale = const Locale('en');
Locale get currentLocale => _currentLocale;
List<Locale> get supportedLocales => AppLocalizations.supportedLocales;
Future<void> setLocale(Locale locale) async {
if (!supportedLocales.contains(locale)) {
throw ArgumentError('Unsupported locale: $locale');
}
_currentLocale = locale;
notifyListeners();
// Persist language preference
await Cache.put('selected_language', locale.languageCode);
}
Future<void> loadSavedLanguage() async {
final savedLanguage = await Cache.get<String>('selected_language');
if (savedLanguage != null) {
final locale = Locale(savedLanguage);
if (supportedLocales.contains(locale)) {
_currentLocale = locale;
notifyListeners();
}
}
}
String getLanguageName(Locale locale) {
switch (locale.languageCode) {
case 'en': return 'English';
case 'ar': return 'العربية';
case 'es': return 'Español';
case 'fr': return 'Français';
default: return locale.languageCode.toUpperCase();
}
}
}
Language Picker Widget¶
class LanguagePicker extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final languageService = ref.watch(languageServiceProvider);
return PopupMenuButton<Locale>(
icon: Icon(Icons.language),
onSelected: (locale) => languageService.setLocale(locale),
itemBuilder: (context) {
return languageService.supportedLocales.map((locale) {
return PopupMenuItem<Locale>(
value: locale,
child: Row(
children: [
Text(languageService.getLanguageName(locale)),
if (locale == languageService.currentLocale)
Padding(
padding: EdgeInsetsDirectional.only(start: AppSpacing.sm),
child: Icon(Icons.check, size: context.iconSmall),
),
],
),
);
}).toList();
},
);
}
}
Testing Localization¶
Localization Tests¶
// test/core/localization/localization_test.dart
void main() {
group('Localization', () {
testWidgets('should display English text correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: Builder(
builder: (context) => Text(context.l10n.welcomeMessage('Test App')),
),
),
);
expect(find.text('Welcome to Test App'), findsOneWidget);
});
testWidgets('should display Arabic text correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('ar'),
home: Builder(
builder: (context) => Text(context.l10n.welcomeMessage('تطبيق تجريبي')),
),
),
);
expect(find.text('مرحباً بك في تطبيق تجريبي'), findsOneWidget);
});
testWidgets('should handle RTL layout correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('ar'),
home: Scaffold(
body: RTLAwareRow(
children: [
Text('First'),
Text('Second'),
],
),
),
),
);
// Verify RTL layout
final scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
expect(scaffold.body, isA<RTLAwareRow>());
});
});
}
Best Practices¶
Naming Conventions¶
- Use descriptive keys:
loginButtoninstead ofbtn1 - Group related strings:
error*,validation*,navigation* - Use camelCase for consistency with Dart conventions
String Organization¶
{
"navigationHome": "Home",
"navigationProfile": "Profile",
"navigationSettings": "Settings",
"authLoginTitle": "Log In",
"authLoginButton": "Log In",
"authLogoutButton": "Log Out",
"authForgotPassword": "Forgot Password",
"errorGeneric": "Something went wrong",
"errorNetwork": "Network error",
"errorValidationRequired": "{field} is required",
"errorValidationEmail": "Invalid email address"
}
Accessibility¶
class AccessibleText extends StatelessWidget {
final String text;
final TextStyle? style;
final String? semanticsLabel;
const AccessibleText(
this.text, {
Key? key,
this.style,
this.semanticsLabel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticsLabel ?? text,
child: Text(text, style: style),
);
}
}
Best Practices¶
- ✅ Use descriptive, hierarchical keys
- ✅ Test with different languages and RTL layouts
- ✅ Provide context in ARB descriptions
- ✅ Handle pluralization properly
- ✅ Consider cultural differences beyond language