方案选择
Flutter对多语言是支持的,不够功能有限,也不是很好用。
GetX中对多语言的支持做得很好,果断选择采用。
GetX中关于多语言介绍
- 继承类Translations,以key,value的形式实现多语言。第一级的key为语言选择,第二级的key为字段标签。最后的value就是最终显示的文本。
import 'package:get/get.dart';
class Messages extends Translations {
@override
Map> get keys => {
'en_US': {
'hello': 'Hello World',
},
'de_DE': {
'hello': 'Hallo Welt',
}
};
}
- 使用的话,只要加.tr后缀就可以,非常方便
Text('hello'.tr);
- 通过@标记,还可以带参数,这个和OC的习惯一致。
import 'package:get/get.dart';
Map> get keys => {
'en_US': {
'logged_in': 'logged in as @name with email @email',
},
'es_ES': {
'logged_in': 'iniciado sesión como @name con e-mail @email',
}
};
Text('logged_in'.trParams({
'name': 'Jhon',
'email': '[email protected]'
}));
- 初始化,使用Translations的地方在程序初始化的地方。这也导致Flutter的热加载不适用于多语言,每次改动都要重新加载,不是很方便。
return GetMaterialApp(
translations: Messages(), // your translations
locale: Locale('en', 'US'), // translations will be displayed in that locale
fallbackLocale: Locale('en', 'UK'), // specify the fallback locale in case an invalid locale is selected.
);
- 切换语言,其实就是更新Locale,更换Translations的第一级key
var locale = Locale('en', 'US');
Get.updateLocale(locale);
- 获取系统语言设置。也就是获取系统的语言key,自动设置。说实话,这作用不大,还不如固定一个默认语言(用在初始化中),然后提供入口进行切换(比如app的设置页面)。
如果非要根据手机的语言设置,自动设置程序的默认语言,那么就可以按照下面的方式来写
return GetMaterialApp(
locale: Get.deviceLocale,
);
- GetX官网介绍
修改方案
所有的语言定义都在Translations一个文件中,很容易出现文件过长的问题。所以会把两级的Map进行拆分,把第二级的Map独立为各个语言文件。
- 将Translations的第二级Map独立成文件
class TranslationService extends Translations {
@override
Map> get keys => {
'en': en_language,
'es': es_language,
'zh': zh_language,
};
}
- 第二级的语言文件就是一个Map
const Map en_language = {
/// 公共部分-基础
WidgetIds.commonBaseTitle: 'Title',
WidgetIds.commonBaseContent: 'Content',
WidgetIds.commonBaseDescription: 'Description',
WidgetIds.commonBaseOk: 'OK',
WidgetIds.commonBaseCancel: 'Cancel',
WidgetIds.commonBaseSubmit: 'Submit',
}
- 各个语言文件的key是公用的,所以集中在一个地方进行定义。
class WidgetIds {
/// 简要说明:
/// 这里定义组件的id
/// 变量定义采用小驼峰的命名习惯
/// 变量的值采用小写加点隔离的方式,类似包名的习惯
/// 可以用来区分组件: dart中组件统一用Widget表示,所以这里命名为WidgetIds
/// 使用场景有多语言,统计等等需要组件定位的场景。
/// 多语言:使用的时候,需要加上.tr后缀,比如: WidgetIds.commonBaseTitle.tr
/// 多语言:定义文件中,作为字典的key来用,比如: WidgetIds.commonBaseTitle: 'Title',
/// 公共部分-基础
static const String commonBaseTitle = 'common.base.title';
static const String commonBaseContent = 'common.base.content';
static const String commonBaseDescription = 'common.base.description';
static const String commonBaseOk = 'common.base.ok';
}
- 相关的文件可以放在一个文件夹中
简略方案
将语言定义独立成单独的key ,value文件,这个保持不变。
组件id定义,也就是语言文件的key定义,与实际的字符显示相差较远。另外定义想名字也是键头疼的事。
使用起来也比较麻烦,像WidgetIds.commonBaseTitle.tr之类的,看上去也不直观。在这种时候,更容易忘记.tr后缀,导致显示'common.base.title'之类的内容。
如果key对应的value没有定义,那么会直接用key的内容来代替,比如Text('hello'.tr); 如果定义了语言文件,会显示定义过的 'Hello World'或者'Hallo Welt'。但是如果没有定义对应的key,value,那么就会直接显示'hello'
我们的应用,默认语言是英语,所以为图方便,我们就直接拿英语做key了,这样还可以少两个文件。widget_ids.dart这key的定义文件就不需要了。
当然,这样做对于那种给@参数的用法就不适用了。凡事有利有弊。当然,要用也是可以的,把两种思路结合起来:大部分用英文作为key;带参数的,就自定义key。
其他的语言文件,直接以英语当做key
const Map zh_language = {
/// 公共部分-基础
'Title': '标题',
'Content': '内容',
'Description': '描述',
'OK': '确定',
'Cancel': '取消',
}
- 适用的地方直接在英语后面加个.tr后缀就可以了。
Text(
'Cancel'.tr,
style: TextStyle(
color: const Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
);
Locale简介
- 构造函数需要两个字符串,语言代码和地区代码。其中语言代码是必选的,地区代码是可选的
const Locale(
this._languageCode, [
this._countryCode,
])
- iOS的手机的菜单路径: 设置 =》通用 =》语言与地区
- 可以理解为语言代码是主键,地区代码是副键。比如同样是中文,也区分中文大陆,中文香港,中文新加坡,中文台湾等。
语言地区代码
- Translation。s中的key,也是以语言代码为主,地区代码为辅。中划线用下划线代替。
另外,也可以简单处理,只用语言代码,不要地区代码。就算地区代码对不上,只要语言代码对了,也能选中。
class MultiLanguage extends Translations {
@override
Map> get keys => {
/// 英文
'en_US': en_US,
/// 中文
'zh_CN': zh_CN,
};
}
更新的话,给Get.updateLocale(const Locale('en', 'TT'));
仍然能够选出英文。
既然如此,干脆key值只给语言代码,不管区域代码。
简单实践
只管语言代码,不管区域代码,简单化处理。当前的需求,只要求有中文版和英文版,还没有那么细。
初始化取手机的系统设置;
默认语言设定为中文
main.dart中的设置
- 字典中只管语言代码,不管区域代码
import 'en.dart';
import 'zh.dart';
class MultiLanguage extends Translations {
@override
Map> get keys => {
/// 英文
'en': en,
/// 中文
'zh': zh,
};
}
/// 中文多语言字典
const Map zh = {
'title': '这是标题',
'login': '登录用户 @name,邮箱账号 @email',
};
/// 英文多语言字典
const Map en = {
'title': 'This is Title!',
'login': 'logged in as @name with email @email',
};
- 切换时只给语言代码,不管区域代码
Get.updateLocale(const Locale('en'));
- 简单例子
代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';
class HomePage extends GetView {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder(
builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text('HomePage'),
centerTitle: true,
),
body: Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.updateLocale(const Locale('en'));
},
child: Text(
'login'.trParams(
{'name': 'zhang san', 'email': '[email protected]'}),
style: const TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
),
),
);
},
);
}
}
初始界面:(手机设置为中文)
点一下文字,就切换为英文
- 关于key的定义
(1)如果想规范一点,那么就用全局变量,按照组件id的的模式统一规范;
(2)如果想方便一点,就用英语作为key。那么en.dart绝大部分可以key和value一样;
zh.dart就算没定义,也会显示有意义的英文,既方便又使用。
我们一开始是用方法(1)的;很规范,但是真的烦。 后来就改成了方法(2);工作量减少很多。
系统日期组件多语言
系统的DateTime组件有一个locale参数,直接加上会崩溃。
如果想要这种系统组件支持多语言,需要额外引入一个库flutter_localizations
参考文章:
Flutter配置国际化localizations
- 日期选择的插件可以考虑试试下面这个(iOS风格的)
flutter_datetime_picker
实践结果
以上内容是一开始时的做法,后来做了修改。
- 命名采用统一命名的方式: “模块.页面.其他限定”,全部用小写字符,单词间用下划线_分割,也就是Linux命名方式。例如:
'order.cancel_order.pop.title': 'Cancel Successfully!',
'order.cancel_order.pop.info1': 'The refund is here',
'order.cancel_order.pop.info2': 'Pandabuy account balance',
订单模块,取消订单,弹窗的标题和信息
本地只保留一份语音文件,作为默认值;
其他多语言文件放后台,到时候通过接口下载。
做一个编辑后台,让其他部门操作,生成新的语言文件。