feat: add flutter-dev skill
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
This commit is contained in:
510
skills/flutter-dev/references/localization.md
Normal file
510
skills/flutter-dev/references/localization.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# Localization
|
||||
|
||||
Internationalization (i18n) patterns using flutter_localizations and intl package for Flutter applications.
|
||||
|
||||
## Setup
|
||||
|
||||
### Dependencies
|
||||
|
||||
```yaml
|
||||
# pubspec.yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: ^0.19.0
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
```
|
||||
|
||||
### l10n Configuration
|
||||
|
||||
```yaml
|
||||
# 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)
|
||||
|
||||
```json
|
||||
// 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
|
||||
|
||||
```json
|
||||
// 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
|
||||
|
||||
```json
|
||||
// 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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
extension LocalizationExtension on BuildContext {
|
||||
AppLocalizations get l10n => AppLocalizations.of(this);
|
||||
}
|
||||
|
||||
// Usage
|
||||
Text(context.l10n.hello)
|
||||
```
|
||||
|
||||
## Dynamic Locale Switching
|
||||
|
||||
### With Riverpod
|
||||
|
||||
```dart
|
||||
@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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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` |
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```json
|
||||
// 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
|
||||
|
||||
```dart
|
||||
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.*
|
||||
Reference in New Issue
Block a user