想象一下,如果你的应用只能被说中文的用户使用,那会失去多少潜在用户?在全球化时代,支持多语言已经不再是"锦上添花",而是"必不可少"。今天我们就来聊聊 Flutter 中的国际化技术,让你的应用能够真正"说"全世界的语言。
🎯 为什么国际化如此重要?
在我开发的一个电商应用中,最初只支持中文。后来我们添加了英文支持,用户量增长了 30%!再后来支持了日语和韩语,用户量又增长了 50%。这让我深刻认识到,语言不是障碍,而是桥梁。
一个成功的国际化应用能够:
- 扩大用户群体:覆盖更多国家和地区的用户
- 提升用户体验:用户使用母语时更舒适
- 增加收入:多语言支持往往带来更多商业机会
- 提升品牌形象:国际化是专业性的体现
一个真实的例子
我们的应用在添加阿拉伯语支持时,遇到了一个有趣的问题:阿拉伯语是从右到左(RTL)书写的。最初我们的界面布局没有考虑这一点,导致阿拉伯语用户反馈"界面看起来很别扭"。后来我们正确实现了 RTL 支持,阿拉伯语用户的满意度提升了 60%!
🚀 从零开始:基础国际化配置
第一步:添加依赖
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.18.0
flutter:
generate: true # 启用代码生成
第二步:配置支持的语言
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '国际化示例',
// 支持的语言列表
supportedLocales: [
Locale('zh', 'CN'), // 简体中文
Locale('zh', 'TW'), // 繁体中文
Locale('en', 'US'), // 美式英语
Locale('en', 'GB'), // 英式英语
Locale('ja', 'JP'), // 日语
Locale('ko', 'KR'), // 韩语
Locale('ar', 'SA'), // 阿拉伯语
],
// 本地化代理
localizationsDelegates: [
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;
}
}
// 如果没有完全匹配,尝试匹配语言代码
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale?.languageCode) {
return supportedLocale;
}
}
// 默认返回第一个支持的语言
return supportedLocales.first;
},
home: MyHomePage(),
);
}
}
📝 创建本地化文件
第三步:定义本地化字符串
// lib/l10n/app_en.arb (英文)
{
"appTitle": "My App",
"@appTitle": {
"description": "The title of the application"
},
"welcomeMessage": "Welcome to our app!",
"@welcomeMessage": {
"description": "Welcome message shown on the home page"
},
"loginButton": "Login",
"@loginButton": {
"description": "Text for the login button"
},
"registerButton": "Register",
"@registerButton": {
"description": "Text for the register button"
},
"emailLabel": "Email",
"@emailLabel": {
"description": "Label for email input field"
},
"passwordLabel": "Password",
"@passwordLabel": {
"description": "Label for password input field"
},
"forgotPassword": "Forgot Password?",
"@forgotPassword": {
"description": "Text for forgot password link"
},
"userGreeting": "Hello, {name}!",
"@userGreeting": {
"description": "Greeting 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 in a list",
"placeholders": {
"count": {
"type": "int",
"example": "5"
}
}
}
}
// lib/l10n/app_zh.arb (中文)
{
"appTitle": "我的应用",
"welcomeMessage": "欢迎使用我们的应用!",
"loginButton": "登录",
"registerButton": "注册",
"emailLabel": "邮箱",
"passwordLabel": "密码",
"forgotPassword": "忘记密码?",
"userGreeting": "你好,{name}!",
"itemCount": "{count, plural, =0{没有项目} =1{1个项目} other{{count}个项目}}"
}
// lib/l10n/app_ja.arb (日语)
{
"appTitle": "マイアプリ",
"welcomeMessage": "アプリへようこそ!",
"loginButton": "ログイン",
"registerButton": "登録",
"emailLabel": "メールアドレス",
"passwordLabel": "パスワード",
"forgotPassword": "パスワードを忘れた?",
"userGreeting": "こんにちは、{name}さん!",
"itemCount": "{count, plural, =0{アイテムなし} =1{1つのアイテム} other{{count}つのアイテム}}"
}
第四步:配置代码生成
# pubspec.yaml
flutter:
generate: true
uses-material-design: true
🎯 在应用中使用国际化
基础用法
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取本地化字符串
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 欢迎消息
Text(
l10n.welcomeMessage,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
SizedBox(height: 32),
// 登录表单
TextFormField(
decoration: InputDecoration(
labelText: l10n.emailLabel,
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: InputDecoration(
labelText: l10n.passwordLabel,
border: OutlineInputBorder(),
),
),
SizedBox(height: 8),
// 忘记密码链接
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {},
child: Text(l10n.forgotPassword),
),
),
SizedBox(height: 24),
// 按钮
ElevatedButton(
onPressed: () {},
child: Text(l10n.loginButton),
),
SizedBox(height: 16),
OutlinedButton(
onPressed: () {},
child: Text(l10n.registerButton),
),
],
),
),
);
}
}
带参数的国际化
class UserProfilePage extends StatelessWidget {
final String userName;
const UserProfilePage({
Key? key,
required this.userName,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 使用带参数的本地化字符串
Text(
l10n.userGreeting(userName),
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 16),
// 使用复数形式的本地化字符串
Text(
l10n.itemCount(5), // 显示 "5个项目"
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
);
}
}
🎨 实战应用:创建国际化组件
1. 语言切换器
class LanguageSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<Locale>(
icon: Icon(Icons.language),
onSelected: (Locale locale) {
// 切换语言
_changeLanguage(context, locale);
},
itemBuilder: (context) => [
PopupMenuItem(
value: Locale('zh', 'CN'),
child: Row(
children: [
Text('🇨🇳 简体中文'),
Spacer(),
if (Localizations.localeOf(context).languageCode == 'zh')
Icon(Icons.check, color: Colors.green),
],
),
),
PopupMenuItem(
value: Locale('en', 'US'),
child: Row(
children: [
Text('🇺🇸 English'),
Spacer(),
if (Localizations.localeOf(context).languageCode == 'en')
Icon(Icons.check, color: Colors.green),
],
),
),
PopupMenuItem(
value: Locale('ja', 'JP'),
child: Row(
children: [
Text('🇯🇵 日本語'),
Spacer(),
if (Localizations.localeOf(context).languageCode == 'ja')
Icon(Icons.check, color: Colors.green),
],
),
),
],
);
}
void _changeLanguage(BuildContext context, Locale locale) {
// 这里需要实现语言切换逻辑
// 通常需要重启应用或使用状态管理
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('语言切换'),
content: Text('语言切换需要重启应用,是否继续?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
ElevatedButton(
onPressed: () {
// 保存语言设置并重启应用
Navigator.pop(context);
// 实现重启逻辑
},
child: Text('确定'),
),
],
),
);
}
}
2. 国际化文本组件
class LocalizedText extends StatelessWidget {
final String key;
final TextStyle? style;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final List<String>? args;
const LocalizedText({
Key? key,
required this.key,
this.style,
this.textAlign,
this.maxLines,
this.overflow,
this.args,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
String text;
switch (key) {
case 'appTitle':
text = l10n.appTitle;
break;
case 'welcomeMessage':
text = l10n.welcomeMessage;
break;
case 'loginButton':
text = l10n.loginButton;
break;
case 'registerButton':
text = l10n.registerButton;
break;
case 'emailLabel':
text = l10n.emailLabel;
break;
case 'passwordLabel':
text = l10n.passwordLabel;
break;
case 'forgotPassword':
text = l10n.forgotPassword;
break;
case 'userGreeting':
text = args != null && args!.isNotEmpty
? l10n.userGreeting(args!.first)
: l10n.userGreeting('User');
break;
case 'itemCount':
int count = args != null && args!.isNotEmpty
? int.tryParse(args!.first) ?? 0
: 0;
text = l10n.itemCount(count);
break;
default:
text = key;
}
return Text(
text,
style: style,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}
3. 日期和数字格式化
class LocalizedFormatters {
// 日期格式化
static String formatDate(BuildContext context, DateTime date) {
final locale = Localizations.localeOf(context);
return DateFormat.yMMMd(locale.languageCode).format(date);
}
// 时间格式化
static String formatTime(BuildContext context, DateTime time) {
final locale = Localizations.localeOf(context);
return DateFormat.Hm(locale.languageCode).format(time);
}
// 货币格式化
static String formatCurrency(BuildContext context, double amount) {
final locale = Localizations.localeOf(context);
return NumberFormat.currency(
locale: locale.languageCode,
symbol: _getCurrencySymbol(locale.languageCode),
).format(amount);
}
// 数字格式化
static String formatNumber(BuildContext context, int number) {
final locale = Localizations.localeOf(context);
return NumberFormat.decimalPattern(locale.languageCode).format(number);
}
// 获取货币符号
static String _getCurrencySymbol(String languageCode) {
switch (languageCode) {
case 'zh':
return '¥';
case 'en':
return '\$';
case 'ja':
return '¥';
case 'ko':
return '₩';
default:
return '\$';
}
}
}
// 使用示例
class ProductCard extends StatelessWidget {
final String name;
final double price;
final DateTime createdAt;
const ProductCard({
Key? key,
required this.name,
required this.price,
required this.createdAt,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(height: 8),
Text(
LocalizedFormatters.formatCurrency(context, price),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'创建时间: ${LocalizedFormatters.formatDate(context, createdAt)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
}
🌐 RTL 语言支持
阿拉伯语支持
class RTLSupportExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isRTL = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(
title: Text('RTL 支持示例'),
leading: isRTL ? IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => Navigator.pop(context),
) : null,
actions: isRTL ? null : [
IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 自适应方向的文本
Text(
'这是自适应方向的文本',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 16),
// 自适应方向的按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: () {},
child: Text('取消'),
),
ElevatedButton(
onPressed: () {},
child: Text('确定'),
),
],
),
SizedBox(height: 16),
// 自适应方向的图标
Row(
children: [
Icon(Icons.star, color: Colors.yellow),
SizedBox(width: 8),
Text('评分: 4.5'),
Spacer(),
Icon(Icons.favorite, color: Colors.red),
SizedBox(width: 8),
Text('喜欢'),
],
),
],
),
),
);
}
}
💡 实用技巧和最佳实践
1. 语言检测和回退
class LanguageManager {
static Locale getPreferredLocale(BuildContext context) {
final locale = Localizations.localeOf(context);
// 检查是否支持当前语言
final supportedLocales = [
Locale('zh', 'CN'),
Locale('en', 'US'),
Locale('ja', 'JP'),
];
// 尝试完全匹配
for (var supported in supportedLocales) {
if (supported.languageCode == locale.languageCode &&
supported.countryCode == locale.countryCode) {
return supported;
}
}
// 尝试语言代码匹配
for (var supported in supportedLocales) {
if (supported.languageCode == locale.languageCode) {
return supported;
}
}
// 默认返回中文
return Locale('zh', 'CN');
}
static String getLanguageName(String languageCode) {
switch (languageCode) {
case 'zh':
return '中文';
case 'en':
return 'English';
case 'ja':
return '日本語';
case 'ko':
return '한국어';
case 'ar':
return 'العربية';
default:
return languageCode.toUpperCase();
}
}
}
2. 动态语言切换
class LanguageProvider extends ChangeNotifier {
Locale _currentLocale = Locale('zh', 'CN');
Locale get currentLocale => _currentLocale;
void changeLanguage(Locale newLocale) {
_currentLocale = newLocale;
notifyListeners();
}
void changeLanguageByCode(String languageCode) {
switch (languageCode) {
case 'zh':
_currentLocale = Locale('zh', 'CN');
break;
case 'en':
_currentLocale = Locale('en', 'US');
break;
case 'ja':
_currentLocale = Locale('ja', 'JP');
break;
default:
_currentLocale = Locale('zh', 'CN');
}
notifyListeners();
}
}
// 在应用中使用
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LanguageProvider(),
child: Consumer<LanguageProvider>(
builder: (context, languageProvider, child) {
return MaterialApp(
locale: languageProvider.currentLocale,
supportedLocales: [
Locale('zh', 'CN'),
Locale('en', 'US'),
Locale('ja', 'JP'),
],
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MyHomePage(),
);
},
),
);
}
}
3. 性能优化
// 缓存本地化字符串
class LocalizedStringCache {
static final Map<String, Map<String, String>> _cache = {};
static String getString(BuildContext context, String key) {
final locale = Localizations.localeOf(context).languageCode;
if (!_cache.containsKey(locale)) {
_cache[locale] = {};
}
if (!_cache[locale]!.containsKey(key)) {
final l10n = AppLocalizations.of(context)!;
// 这里需要根据key获取对应的字符串
_cache[locale]![key] = _getStringByKey(l10n, key);
}
return _cache[locale]![key]!;
}
static String _getStringByKey(AppLocalizations l10n, String key) {
switch (key) {
case 'appTitle':
return l10n.appTitle;
case 'welcomeMessage':
return l10n.welcomeMessage;
// 添加更多key的处理
default:
return key;
}
}
static void clearCache() {
_cache.clear();
}
}
📚 总结
国际化是让应用走向世界的重要一步。通过合理使用 Flutter 的国际化功能,我们可以:
- 扩大用户群体:支持多语言,覆盖更多用户
- 提升用户体验:用户使用母语时更舒适
- 增加商业机会:多语言支持往往带来更多收入
- 提升品牌形象:国际化是专业性的体现
关键要点
- 基础配置 是国际化的第一步,需要正确设置依赖和语言支持
- 本地化文件 是核心,需要为每种语言创建对应的字符串文件
- RTL 支持 对于阿拉伯语等从右到左的语言很重要
- 性能优化 对于大量文本的应用尤为重要
下一步学习
掌握了国际化的基础后,你可以继续学习:
记住,好的国际化不仅仅是翻译文本,更重要的是理解不同文化的用户习惯和需求。在实践中不断优化,你一定能创建出真正国际化的应用!