Add comprehensive Flutter cross-platform development guide covering: - Widget patterns and const optimization - Riverpod/Bloc state management - GoRouter navigation - Performance optimization - Testing strategies - Platform-specific implementations Made-with: Cursor
12 KiB
12 KiB
Localization
Internationalization (i18n) patterns using flutter_localizations and intl package for Flutter applications.
Setup
Dependencies
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: true
l10n Configuration
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
ARB Files
English (Template)
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The application title"
},
"hello": "Hello",
"welcome": "Welcome, {name}!",
"@welcome": {
"description": "Welcome message with user name",
"placeholders": {
"name": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Number of items",
"placeholders": {
"count": {
"type": "int"
}
}
},
"lastUpdated": "Last updated: {date}",
"@lastUpdated": {
"description": "Last update timestamp",
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
"price": "Price: {amount}",
"@price": {
"description": "Product price",
"placeholders": {
"amount": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "$",
"decimalDigits": 2
}
}
}
},
"gender": "{gender, select, male{He} female{She} other{They}} liked this",
"@gender": {
"description": "Gender-specific message",
"placeholders": {
"gender": {
"type": "String"
}
}
}
}
Chinese
// lib/l10n/app_zh.arb
{
"@@locale": "zh",
"appTitle": "我的应用",
"hello": "你好",
"welcome": "欢迎,{name}!",
"itemCount": "{count, plural, =0{没有项目} other{{count} 个项目}}",
"lastUpdated": "最后更新:{date}",
"price": "价格:{amount}",
"gender": "{gender, select, male{他} female{她} other{Ta}}喜欢了这个"
}
Japanese
// lib/l10n/app_ja.arb
{
"@@locale": "ja",
"appTitle": "マイアプリ",
"hello": "こんにちは",
"welcome": "ようこそ、{name}さん!",
"itemCount": "{count, plural, =0{アイテムなし} other{{count}件}}",
"lastUpdated": "最終更新:{date}",
"price": "価格:{amount}",
"gender": "{gender, select, male{彼} female{彼女} other{その人}}がいいねしました"
}
App Configuration
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('zh'),
Locale('ja'),
],
locale: const Locale('en'),
home: const HomePage(),
);
}
}
Using Translations
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
Text(l10n.hello),
Text(l10n.welcome('John')),
Text(l10n.itemCount(5)),
Text(l10n.lastUpdated(DateTime.now())),
Text(l10n.price(29.99)),
Text(l10n.gender('female')),
],
),
);
}
}
Extension for Convenience
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
// Usage
Text(context.l10n.hello)
Dynamic Locale Switching
With Riverpod
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
final saved = ref.watch(sharedPreferencesProvider).getString('locale');
if (saved != null) {
return Locale(saved);
}
return const Locale('en');
}
void setLocale(Locale locale) {
ref.read(sharedPreferencesProvider).setString('locale', locale.languageCode);
state = locale;
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
home: const HomePage(),
);
}
}
Language Selector
class LanguageSelector extends ConsumerWidget {
const LanguageSelector({super.key});
static const languages = [
(Locale('en'), 'English'),
(Locale('zh'), '中文'),
(Locale('ja'), '日本語'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeNotifierProvider);
return PopupMenuButton<Locale>(
initialValue: currentLocale,
onSelected: (locale) {
ref.read(localeNotifierProvider.notifier).setLocale(locale);
},
itemBuilder: (context) => languages.map((lang) {
return PopupMenuItem(
value: lang.$1,
child: Row(
children: [
if (currentLocale == lang.$1)
const Icon(Icons.check, size: 18),
const SizedBox(width: 8),
Text(lang.$2),
],
),
);
}).toList(),
child: const Icon(Icons.language),
);
}
}
Date and Number Formatting
import 'package:intl/intl.dart';
class FormattingUtils {
static String formatDate(DateTime date, String locale) {
return DateFormat.yMMMd(locale).format(date);
}
static String formatDateTime(DateTime dateTime, String locale) {
return DateFormat.yMMMd(locale).add_jm().format(dateTime);
}
static String formatRelativeTime(DateTime dateTime, String locale) {
final now = DateTime.now();
final diff = now.difference(dateTime);
if (diff.inDays > 7) {
return DateFormat.yMMMd(locale).format(dateTime);
} else if (diff.inDays > 0) {
return '${diff.inDays}d ago';
} else if (diff.inHours > 0) {
return '${diff.inHours}h ago';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}m ago';
} else {
return 'Just now';
}
}
static String formatCurrency(double amount, String locale, {String? symbol}) {
return NumberFormat.currency(
locale: locale,
symbol: symbol,
decimalDigits: 2,
).format(amount);
}
static String formatNumber(num number, String locale) {
return NumberFormat.decimalPattern(locale).format(number);
}
static String formatPercent(double value, String locale) {
return NumberFormat.percentPattern(locale).format(value);
}
static String formatCompact(num number, String locale) {
return NumberFormat.compact(locale: locale).format(number);
}
}
Usage with Locale
class FormattedContent extends StatelessWidget {
const FormattedContent({super.key});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
return Column(
children: [
Text(FormattingUtils.formatDate(DateTime.now(), locale)),
Text(FormattingUtils.formatCurrency(1234.56, locale, symbol: '\$')),
Text(FormattingUtils.formatNumber(1234567, locale)),
Text(FormattingUtils.formatPercent(0.75, locale)),
Text(FormattingUtils.formatCompact(1500000, locale)),
],
);
}
}
RTL Support
class RtlAwareWidget extends StatelessWidget {
const RtlAwareWidget({super.key});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Row(
children: [
Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward),
const Expanded(child: Text('Content')),
Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: const Icon(Icons.settings),
),
],
);
}
}
Directional Widgets
| Standard | Directional |
|---|---|
EdgeInsets |
EdgeInsetsDirectional |
Padding |
Padding with EdgeInsetsDirectional |
Align |
AlignmentDirectional |
Positioned |
PositionedDirectional |
BorderRadius |
BorderRadiusDirectional |
// Use directional
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: child,
)
Container(
alignment: AlignmentDirectional.centerStart,
child: child,
)
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(8),
bottomStart: Radius.circular(8),
),
),
)
Organized Translations
Split by Feature
lib/
l10n/
app_en.arb # Common translations
app_zh.arb
features/
auth_en.arb # Auth feature translations
auth_zh.arb
settings_en.arb # Settings feature translations
settings_zh.arb
Namespaced Keys
// app_en.arb
{
"auth_login": "Login",
"auth_logout": "Logout",
"auth_forgotPassword": "Forgot Password?",
"settings_title": "Settings",
"settings_language": "Language",
"settings_theme": "Theme",
"error_network": "Network error. Please try again.",
"error_unknown": "An unknown error occurred."
}
Testing
void main() {
testWidgets('shows localized text', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const HomePage(),
),
);
expect(find.text('Hello'), findsOneWidget);
});
testWidgets('switches locale', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('zh'),
home: const HomePage(),
),
),
);
expect(find.text('你好'), findsOneWidget);
});
}
ARB Placeholders Reference
| Type | Format Options |
|---|---|
String |
None |
int |
compact, compactCurrency, compactLong, compactSimpleCurrency |
double |
compact, compactCurrency, currency, decimalPattern, decimalPercentPattern, percentPattern, scientificPattern, simpleCurrency |
DateTime |
Any DateFormat pattern (yMd, yMMMd, jm, etc.) |
num |
Same as int and double |
Localization Checklist
| Item | Implementation |
|---|---|
| Dependencies | flutter_localizations, intl |
| l10n.yaml | Configure ARB paths and output |
| ARB files | Create for each supported locale |
| App config | Add delegates and supported locales |
| Generate | Run flutter gen-l10n |
| Use translations | AppLocalizations.of(context) |
| Date/number formatting | Use intl formatters with locale |
| RTL support | Use directional widgets |
| Persist preference | Save user's locale choice |
| Testing | Test with different locales |
Flutter is a trademark of Google LLC. intl is an open-source package by the Dart team.