Files
mljxxx 2995582a5e 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
2026-03-26 17:35:55 +08:00

12 KiB
Raw Permalink Blame History

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.