跳转到正文
YGJoon 技术博客

Flutter 国际化实现详解

发布于:
·1.3k 字 · 约 3 分钟·作者:
intl + flutter_localizations 配好 delegates,ARB 管翻译文案,Locale 切换语言,复数占位符格式一次搞定
Flutter 国际化实现详解文章封面
目录

简介

国际化(i18n)是应用程序适配不同语言和地区的过程。Flutter 提供了完整的国际化支持,可以轻松实现多语言切换。

基本配置

添加依赖

dependencies:
  flutter_localizations:
    sdk: flutter
  intl: ^0.18.1

配置 MaterialApp

MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    AppLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', ''), // English
    Locale('zh', ''), // Chinese
    Locale('es', ''), // Spanish
  ],
  locale: _currentLocale, // 当前语言
)

实现本地化

定义翻译

class AppLocalizations {
  final Locale locale;
  
  AppLocalizations(this.locale);
  
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(
      context,
      AppLocalizations,
    )!;
  }
  
  static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();
      
  static Map<String, Map<String, String>> _localizedValues = {
    'en': {
      'title': 'My App',
      'hello': 'Hello',
      'settings': 'Settings',
      'language': 'Language',
      'theme': 'Theme',
    },
    'zh': {
      'title': '我的应用',
      'hello': '你好',
      'settings': '设置',
      'language': '语言',
      'theme': '主题',
    },
    'es': {
      'title': 'Mi Aplicación',
      'hello': 'Hola',
      'settings': 'Ajustes',
      'language': 'Idioma',
      'theme': 'Tema',
    },
  };
  
  String get title => _localizedValues[locale.languageCode]!['title']!;
  String get hello => _localizedValues[locale.languageCode]!['hello']!;
  String get settings => _localizedValues[locale.languageCode]!['settings']!;
  String get language => _localizedValues[locale.languageCode]!['language']!;
  String get theme => _localizedValues[locale.languageCode]!['theme']!;
}

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

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

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

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

语言切换

语言管理

class LanguageProvider extends ChangeNotifier {
  Locale _locale = Locale('en');
  
  Locale get locale => _locale;
  
  void setLocale(Locale locale) {
    if (!AppLocalizations.delegate.isSupported(locale)) return;
    _locale = locale;
    notifyListeners();
  }
  
  // 获取当前语言名称
  String getCurrentLanguageName() {
    switch (_locale.languageCode) {
      case 'en':
        return 'English';
      case 'zh':
        return '中文';
      case 'es':
        return 'Español';
      default:
        return 'English';
    }
  }
}

语言持久化

class LanguagePreferences {
  static const String key = 'language_code';
  
  static Future<void> setLanguageCode(String code) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, code);
  }
  
  static Future<String> getLanguageCode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key) ?? 'en';
  }
}

完整示例

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => LanguageProvider(),
      child: Consumer<LanguageProvider>(
        builder: (context, languageProvider, child) {
          return MaterialApp(
            localizationsDelegates: [
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
              AppLocalizations.delegate,
            ],
            supportedLocales: [
              Locale('en', ''),
              Locale('zh', ''),
              Locale('es', ''),
            ],
            locale: languageProvider.locale,
            home: HomePage(),
          );
        },
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final localizations = AppLocalizations.of(context);
    final languageProvider = Provider.of<LanguageProvider>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text(localizations.title),
      ),
      body: ListView(
        children: [
          ListTile(
            title: Text(localizations.language),
            subtitle: Text(languageProvider.getCurrentLanguageName()),
            onTap: () => _showLanguageDialog(context),
          ),
          Divider(),
          ListTile(
            title: Text(localizations.hello),
          ),
        ],
      ),
    );
  }
  
  void _showLanguageDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text(AppLocalizations.of(context).language),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                title: Text('English'),
                onTap: () {
                  Provider.of<LanguageProvider>(context, listen: false)
                      .setLocale(Locale('en'));
                  Navigator.pop(context);
                },
              ),
              ListTile(
                title: Text('中文'),
                onTap: () {
                  Provider.of<LanguageProvider>(context, listen: false)
                      .setLocale(Locale('zh'));
                  Navigator.pop(context);
                },
              ),
              ListTile(
                title: Text('Español'),
                onTap: () {
                  Provider.of<LanguageProvider>(context, listen: false)
                      .setLocale(Locale('es'));
                  Navigator.pop(context);
                },
              ),
            ],
          ),
        );
      },
    );
  }
}

最佳实践

  1. 文案模板用命名占位符("{name} 购买了 {item}"),别字符串拼接:德语、日语的语序和中文不一样,按中文语序拼死的句子翻译过去就是病句。
  2. 复数用 Intl.plural,别写三元表达式:中文没有复数变化,但俄语有 3 种复数形式、阿拉伯语有 6 种——count == 1 ? 'item' : 'items' 这种写法一上多语言就崩。
  3. 日期、数字、货币一律走 intl 的 DateFormat/NumberFormat:别手拼 "2024-01-01",美国人看不懂,德国人的小数点还是逗号。

注意事项

  1. 翻译文本长度:德语比英语长 30% 左右,按钮、Tab 写死宽度必溢出;用 Flexible、softWrap、FittedBox,别写死 120.0 这种宽度——每加一种语言都要跑一遍 UI 走查。
  2. 中文特有坑:“5 分钟前”这种相对时间别直译英文模板("5 minutes ago" 翻成”5 分钟之前”读着别扭),用 timeago 包或自己写一套符合中文习惯的表达。
  3. RTL 语言(阿拉伯语、希伯来语):布局基本自动镜像,但 EdgeInsets 要用 EdgeInsetsDirectional——left/right 写死会导致镜像错乱;自定义绘制和绝对定位的角标要手动处理。
  4. 切语言会重建整个 MaterialApp(locale 变化整树 rebuild):这是预期行为不是 bug——别在语言切换时做耗时操作,也别指望切语言后页面滚动位置还在。

实战案例:只做中文也要加 flutter_localizations

旅行类应用 是个只做中文的电商 App,但它的 pubspec 里照样有 flutter_localizations。为什么?因为这不是”做多语言才加”的依赖,而是”用了 Material 组件就得加”——GlobalMaterialLocalizations.delegate 管的是日期选择器、TextField 长按菜单这些系统文案的本地化,不加的话,中文手机上弹出的日期选择器照样显示英文月份。我最早也以为单语言 App 可以省掉它,直到在一个项目里看到英文的”Cancel/OK”按钮才去补。trade-off 在文案管理上:文案少的时候(几十条),直接写个 Map 放代码里最省事,别急着上 arb;文案一多、或者以后要接翻译平台,再迁到 flutter_intl + arb 工具链。过早引入 arb 工具链,改一条文案要跑一遍生成,对小团队是纯负担。先让 App 跑起来,再谈规范化。

总结

通过合理使用 Flutter 的国际化支持,可以轻松实现应用的多语言支持。注意在实现过程中要考虑文本适配和布局问题。


分享这篇文章:


相关文章

上一篇
Flutter Stack 组件详解
下一篇
Flutter 权限处理详解