Flutter 国际化与本地化完全指南

引言

国际化(Internationalization)和本地化(Localization)是构建全球应用的关键。Flutter 提供了强大的国际化支持。本文将深入探讨如何实现应用的多语言支持。

基础概念回顾

国际化 vs 本地化

  • 国际化 (i18n): 设计应用使其能够适应不同语言和地区
  • 本地化 (l10n): 为特定语言和地区定制应用

核心组件

  • Localizations: 本地化组件
  • LocalizationsDelegate: 本地化委托
  • GlobalMaterialLocalizations: Material组件本地化
  • GlobalWidgetsLocalizations: Widget本地化

高级技巧一:基础配置

添加依赖

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.18.1

配置 MaterialApp

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      supportedLocales: const [
        Locale('zh', 'CN'),
        Locale('en', 'US'),
        Locale('ja', 'JP'),
      ],
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      localeResolutionCallback: (locale, supportedLocales) {
        for (var supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale?.languageCode &&
              supportedLocale.countryCode == locale?.countryCode) {
            return supportedLocale;
          }
        }
        return supportedLocales.first;
      },
      home: const HomeScreen(),
    );
  }
}

高级技巧二:创建本地化类

使用 intl 工具生成

import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';

class AppLocalizations {
  AppLocalizations(this.locale);

  final Locale locale;

  static AppLocalizations? of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }

  static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();

  Map<String, String> _localizedStrings = {};

  Future<void> load() async {
    String jsonString = await Future.delayed(
      const Duration(milliseconds: 100),
      () => '{"hello": "Hello", "welcome": "Welcome"}',
    );
    // 实际项目中从assets加载
    Map<String, dynamic> jsonMap = {};
    _localizedStrings = jsonMap.map((key, value) => MapEntry(key, value.toString()));
  }

  String translate(String key) {
    return _localizedStrings[key] ?? key;
  }
}

class _AppLocalizationsDelegate
    extends LocalizationsDelegate<AppLocalizations> {
  const _AppLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) {
    return ['en', 'zh', 'ja'].contains(locale.languageCode);
  }

  @override
  Future<AppLocalizations> load(Locale locale) async {
    AppLocalizations localizations = AppLocalizations(locale);
    await localizations.load();
    return localizations;
  }

  @override
  bool shouldReload(_AppLocalizationsDelegate old) => false;
}

使用 .arb 文件

创建 l10n/intl_en.arb:

{
  "@@locale": "en",
  "hello": "Hello",
  "welcome": "Welcome to our app",
  "greeting": "Hello {name}",
  "count": "{count} items"
}

创建 l10n/intl_zh.arb:

{
  "@@locale": "zh",
  "hello": "你好",
  "welcome": "欢迎来到我们的应用",
  "greeting": "你好 {name}",
  "count": "{count} 个项目"
}

高级技巧三:使用生成的代码

配置 pubspec.yaml

flutter:
  generate: true
  assets:
    - lib/l10n/

创建 l10n.yaml

arb-dir: lib/l10n
template-arb-file: intl_en.arb
output-localization-file: app_localizations.dart

使用生成的本地化类

Text(AppLocalizations.of(context)!.hello);
Text(AppLocalizations.of(context)!.greeting('John'));
Text(AppLocalizations.of(context)!.count(5));

高级技巧四:动态切换语言

创建语言切换器

class LanguageProvider with ChangeNotifier {
  Locale _locale = const Locale('en');

  Locale get locale => _locale;

  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }
}

使用 Provider

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => LanguageProvider(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<LanguageProvider>(
      builder: (context, provider, child) {
        return MaterialApp(
          locale: provider.locale,
          supportedLocales: const [
            Locale('en', 'US'),
            Locale('zh', 'CN'),
            Locale('ja', 'JP'),
          ],
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          home: const HomeScreen(),
        );
      },
    );
  }
}

创建语言选择器

class LanguageSelector extends StatelessWidget {
  const LanguageSelector({super.key});

  @override
  Widget build(BuildContext context) {
    return DropdownButton<Locale>(
      value: Provider.of<LanguageProvider>(context).locale,
      items: const [
        DropdownMenuItem(value: Locale('en', 'US'), child: Text('English')),
        DropdownMenuItem(value: Locale('zh', 'CN'), child: Text('中文')),
        DropdownMenuItem(value: Locale('ja', 'JP'), child: Text('日本語')),
      ],
      onChanged: (locale) {
        if (locale != null) {
          Provider.of<LanguageProvider>(context, listen: false).setLocale(locale);
        }
      },
    );
  }
}

高级技巧五:格式化日期和数字

日期格式化

String formatDate(DateTime date) {
  return DateFormat.yMd(AppLocalizations.of(context)!.localeName).format(date);
}

String formatFullDate(DateTime date) {
  return DateFormat.yMMMMd(AppLocalizations.of(context)!.localeName).format(date);
}

数字格式化

String formatNumber(int number) {
  return NumberFormat.decimalPattern(AppLocalizations.of(context)!.localeName)
      .format(number);
}

String formatCurrency(double amount) {
  return NumberFormat.currency(
    locale: AppLocalizations.of(context)!.localeName,
    symbol: '\$',
  ).format(amount);
}

实战案例:完整的国际化应用

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    
    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.welcome),
        actions: const [LanguageSelector()],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(l10n.hello),
            const SizedBox(height: 16),
            Text(l10n.greeting('Flutter')),
            const SizedBox(height: 16),
            Text(l10n.count(10)),
            const SizedBox(height: 32),
            Text('Today: ${formatDate(DateTime.now())}'),
            const SizedBox(height: 16),
            Text('Number: ${formatNumber(1234567)}'),
            const SizedBox(height: 16),
            Text('Price: ${formatCurrency(99.99)}'),
          ],
        ),
      ),
    );
  }
}

实战案例:本地化验证消息

class Validators {
  static String? validateEmail(String? value, BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    
    if (value == null || value.isEmpty) {
      return l10n.requiredField;
    }
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
      return l10n.invalidEmail;
    }
    return null;
  }

  static String? validatePassword(String? value, BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    
    if (value == null || value.isEmpty) {
      return l10n.requiredField;
    }
    if (value.length < 8) {
      return l10n.passwordTooShort;
    }
    return null;
  }
}

实战案例:RTL 支持

Directionality(
  textDirection: TextDirection.rtl,
  child: Text('سلام'),
)

检测 RTL

bool isRTL(Locale locale) {
  return ['ar', 'he', 'fa', 'ur'].contains(locale.languageCode);
}

常见问题与解决方案

Q1:如何处理复数形式?

A:使用 intl 包的 PluralRules:

int count = 5;
String message = Intl.plural(
  count,
  zero: 'no items',
  one: '1 item',
  other: '$count items',
);

Q2:如何处理性别?

A:使用 intl 包的 GenderRules:

String message = Intl.gender(
  'male',
  male: 'He',
  female: 'She',
  other: 'They',
);

Q3:如何预加载本地化数据?

A:在启动时提前加载:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 预加载本地化数据
  await AppLocalizations(const Locale('en')).load();
  
  runApp(const MyApp());
}

最佳实践

1. 使用 .arb 文件管理翻译

flutter:
  generate: true
  assets:
    - lib/l10n/

2. 创建翻译管理类

class Translation {
  static String get hello => AppLocalizations.of(navigatorKey.currentContext!)!.hello;
  static String get welcome => AppLocalizations.of(navigatorKey.currentContext!)!.welcome;
}

3. 使用 context 扩展

extension BuildContextExtension on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this)!;
}

// 使用
Text(context.l10n.hello);

总结

Flutter 的国际化和本地化功能非常强大。通过本文的学习,你应该能够:

  1. 配置基本的国际化支持
  2. 创建和管理翻译文件
  3. 使用 intl 工具生成代码
  4. 动态切换语言
  5. 格式化日期和数字
  6. 支持 RTL 语言

掌握这些技巧,能够帮助你构建真正全球化的应用。

更多推荐