持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
本文翻译自pub:slang | Dart Package (flutter-io.cn)
译时版本: slang: 3.0.0
slang
[s]tructured [lan]guage file [g]enerator
类型安全的国际化方案,使用 JSON 、 YAML 或 CSV 文件。
fast_i18n的官方接班库。
关于该库
- 🚀 最小必要准备、创建 JSON 文件后就可以开始!无需配置。
- 🐞 Bug抑制,不会有因为编译时校验导致输出错误或缺失参数的可能。
- ⚡ 快速、使用原生 Dart 方法调用生成译文,零解析!
- 📁 组织化、可通过命名空间将大文件分割成更小的文件。
- 🔨 可配置,英语不是默认语言?在
build.yaml中配置!
可在这里看一下生成文件的示例。
下面是如何访问译文:
final t = Translations.of(context); // 也有不带 context 的静态 getter
String a = t.mainScreen.title; // 简单使用
String b = t.game.end.highscore(score: 32.6); // 使用参数
String c = t.items(count: 2); // 使用多元化
String d = t.greet(name: 'Tom', context: Gender.male); // 使用自定义上下文
String e = t.intro.step[4]; // 使用下标
String f = t.error.type['WARNING']; // 使用动态键
String g = t['mainScreen.title']; // 使用完整的动态键
TextSpan h = t.greet(name: TextSpan(text: 'Tom')); // 使用 RichText
PageData page0 = t.onboarding.pages[0]; // 使用接口
PageData page1 = t.onboarding.pages[1];
String h = page1.title; // 类型安全调用
文章目录
开始
参考母语的向导会更容易。
来自 ARB? 这有一个用于它的 工具 。
第 1 步: 添加依赖
可能至少需要两个包:slang 和 slang_flutter。
dependencies:
slang: <version>
slang_flutter: <version> # 如果使用 Flutter,也要添加该包
dev_dependencies:
build_runner: <version> # 如果使用 build_runner (1/2)
slang_build_runner: <version> # 如果使用 build_runner (2/2)
第 2 步: 创建 JSON 文件
在 lib 库中创建这些文件。例如, lib/i18n 。
也支持 YAML 和 CSV 文件(参考 文件类型)。
将译文写入到 asset 目录中需要额外的配置(参考常见问题)。
格式:
<namespace>_<locale?>.<extension>
对于该基础示例可以忽略 命名空间,所以可只使用普通的名字如 strings 或 translations。
示例:
lib/
└── i18n/
└── strings.i18n.json
└── strings_de.i18n.json
└── strings_zh-CN.i18n.json <-- 国家(地区)码示例
// 文件: strings.i18n.json (强制性,对应基本的语言)
{
"hello": "Hello $name",
"save": "Save",
"login": {
"success": "Logged in successfully",
"fail": "Logged in failed"
}
}
// 文件: strings_de.i18n.json
{
"hello": "Hallo $name",
"save": "Speichern",
"login": {
"success": "Login erfolgreich",
"fail": "Login fehlgeschlagen"
}
}
第 3 步: 生成 Dart 代码
运行内置命令:
flutter pub run slang
代替命令:(需要slang_build_runner):
flutter pub run build_runner build --delete-conflicting-outputs
第 4 步: 初始化
a) 使用设备语言
void main() {
WidgetsFlutterBinding.ensureInitialized(); // 添加该行
LocaleSettings.useDeviceLocale(); // 和该行
runApp(MyApp());
}
b) 使用指定语言
@override
void initState() {
super.initState();
String storedLocale = loadFromStorage(); // 你的逻辑
LocaleSettings.setLocaleRaw(storedLocale);
}
c) 使用依赖注入 (也称作 "我自己来处理" )
final english = AppLocale.en.build();
final german = AppLocale.de.build();
// 读取
String a = german.login.success;
如果你要自己处理语言,可以忽略步骤 4a 和 5(但不能忽略 4b)。
第 4a 步: Flutter 语言
该步骤可选,也建议做一下。
标准的 Flutter 组件(例:后退按钮的提示框)会使用正确的语言。
# 文件: pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations: # 添加该行
sdk: flutter
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(TranslationProvider(child: MyApp())); // 使用TranslationProvider包裹 APP
}
MaterialApp(
locale: TranslationProvider.of(context).flutterLocale, // 使用 provider
supportedLocales: LocaleSettings.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
child: YourFirstScreen(),
)
第 4b 步: iOS 配置
文件: ios/Runner/Info.plist
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>de</string>
</array>
第 5 步: 使用译文
import 'package:my_app/i18n/strings.g.dart'; // 导入
String a = t.login.success; // 获取译文
配置
该步骤是可选的。该库无需任何配置即可使用(大多数情况)。
对于自定义,可以创建 slang.yaml 或 build.yaml 文件,将其放在根目录下。
slang.yaml
如果不想使用 build_runner ,可在 slang.yaml 中定义配置减少模板代码。
base_locale: fr
fallback_strategy: base_locale
input_directory: lib/i18n
input_file_pattern: .i18n.json
output_directory: lib/i18n
output_file_name: translations.g.dart
output_format: single_file
locale_handling: true
flutter_integration: true
namespaces: false
translate_var: t
enum_name: AppLocale
translation_class_visibility: private
key_case: snake
key_map_case: camel
param_case: pascal
string_interpolation: double_braces
flat_map: false
translation_overrides: false
timestamp: true
maps:
- error.codes
- category
- iconNames
pluralization:
auto: cardinal
default_parameter: n
cardinal:
- someKey.apple
ordinal:
- someKey.place
contexts:
gender_context:
enum:
- male
- female
paths:
- my.path.to.greet
default_parameter: gender
generate_enum: true
interfaces:
PageData: onboarding.pages.*
PageData2:
paths:
- my.path
- cool.pages.*
attributes:
- String title
- String? content
imports:
- 'package:my_package/path_to_enum.dart'
build.yaml
如果使用 build_runner , build.yaml 是必须的。
targets:
$default:
builders:
slang_build_runner:
options:
base_locale: fr
fallback_strategy: base_locale
input_directory: lib/i18n
input_file_pattern: .i18n.json
output_directory: lib/i18n
output_file_name: translations.g.dart
output_format: single_file
locale_handling: true
flutter_integration: true
namespaces: false
translate_var: t
enum_name: AppLocale
translation_class_visibility: private
key_case: snake
key_map_case: camel
param_case: pascal
string_interpolation: double_braces
flat_map: false
translation_overrides: false
timestamp: true
maps:
- error.codes
- category
- iconNames
pluralization:
auto: cardinal
default_parameter: n
cardinal:
- someKey.apple
ordinal:
- someKey.place
contexts:
gender_context:
enum:
- male
- female
paths:
- my.path.to.greet
default_parameter: gender
generate_enum: true
interfaces:
PageData: onboarding.pages.*
PageData2:
paths:
- my.path
- cool.pages.*
attributes:
- String title
- String? content
imports:
- 'package:my_package/path_to_enum.dart'
| 键 | 类型 | 用法 | 默认 |
|---|---|---|---|
base_locale | String | 默认 json 的语言 | en |
fallback_strategy | none, base_locale | 处理缺失的译文 (i) | none |
input_directory | String | 输入目录的路径 | null |
input_file_pattern | String | 输入文件的模式(文件名格式),必须以 .json 、 .yaml 或 .csv 结尾 | .i18n.json |
output_directory | String | 输出目录的路径 | null |
output_file_name | String | 输出文件名 | null |
output_format | single_file, multiple_files | 切割的输出文件 (i) | single_file |
locale_handling | Boolean | 生成语言的处理逻辑 (i) | true |
flutter_integration | Boolean | 生成 flutter 特性 (i) | true |
namespaces | Boolean | 分割输入文件 (i) | false |
translate_var | String | 翻译变量名 | t |
enum_name | String | 枚举名 | AppLocale |
translation_class_visibility | private, public | 类的可见性 visibility | private |
key_case | null, camel, pascal, snake | 转换键(可选) (i) | null |
key_map_case | null, camel, pascal, snake | 从 Map 中转换 key (可选) (i) | null |
param_case | null, camel, pascal, snake | 转换参数 (可选) (i) | null |
string_interpolation | dart, braces, double_braces | 字符串插值模式 (i) | dart |
flat_map | Boolean | 生成单层 Map (i) | true |
translation_overrides | Boolean | 允许覆写译文 (i) | false |
timestamp | Boolean | 写入 "Built on" 时间戳 | true |
maps | List<String> | 应该通过键访问的实体(Map) (i) | [] |
pluralization/auto | off, cardinal, ordinal | 自动检测复数 (i) | cardinal |
pluralization/default_parameter | String | 默认的复数参数 (i) | n |
pluralization/cardinal | List<String> | 有基数内容的实体 | [] |
pluralization/ordinal | List<String> | 有序数的实体 | [] |
<context>/enum | List<String> | context 格式 (i) | 无默认值 |
<context>/auto | Boolean | 自动检测 context | true |
<context>/paths | List<String> | 使用该 context 的实体 | [] |
<context>/default_parameter | String | 默认的参数名 | context |
<context>/generate_enum | Boolean | 生成枚举 | true |
children of interfaces | Pairs of Alias:Path | 别名接口 (i) | null |
imports | List<String> | 生成 import 语句 | [] |
主要特性
➤ 文件类型
支持的文件类型:JSON (默认) 、 YAML 和 CSV 。
要改为 YAML 或 CSV ,请修改 input_file_pattern 。
# 配置
input_directory: assets/i18n
input_file_pattern: .i18n.yaml # 必须以 .json 、 .yaml 或 .csv 结尾
JSON 示例
{
"welcome": {
"title": "Welcome $name"
}
}
YAML 示例
welcome:
title: Welcome $name # 一些注释
CSV 示例
行结束符必须是 CRLF 。
也可以将多个语言绑定到一个 CSV(查看压缩 CSV)。
# 格式: <key>, <translation>
welcome.title,Welcome $name
pages.0.title,First Page
pages.1.title,Second Page
➤ 字符串插值
译文通常带有动态参数。有多种方式可以定义它们。
# 配置
string_interpolation: dart # 改为大括号或双大括号
通常也可以添加反斜线来转义,如 \{notAnArgument}。
dart (默认)
Hello $name. I am ${height}m.
大括号
Hello {name}
双大括号
Hello {{name}}
➤ 富文本
想要部分文本粗体显示或改为不同的颜色?需要内部链接?
TextSpan可提供帮助!
要这样做,请添加 (rich) 修饰符。
参数会通过 string_interpolation 被格式化。
默认的文本可用小括号定义,例:underline(here) 。
{
"myText(rich)": "Welcome $name. Please click ${underline(here)}!"
}
用法:
// Text.rich 是 Flutter 内置的特性!
Widget a = Text.rich(t.myText(
// 用蓝色显示 name
name: TextSpan(text: 'Tom', style: TextStyle(color: Colors.blue)),
// 将 'here' 转为链接
underline: (text) => TextSpan(
text: text,
style: TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()..onTap=(){
print('tap');
},
),
));
➤ 列表
完全支持列表。无需配置。也可以在列表中再添加列表或 Map !
{
"niceList": [
"hello",
"nice",
[
"first item in nested list",
"second item in nested list"
],
{
"wow": "WOW!",
"ok": "OK!"
},
{
"a map entry": "access via key",
"another entry": "access via second key"
}
]
}
String a = t.niceList[1]; // "nice"
String b = t.niceList[2][0]; // "first item in nested list"
String c = t.niceList[3].ok; // "OK!"
String d = t.niceList[4]['a map entry']; // "access via key"
➤ Map
定义 Map ,可以通过键访问各自的译文。
添加 (map) 修饰符。
// 文件: strings.i18n.json
{
"a(map)": {
"hello world": "hello"
},
"b": {
"b0": "hey",
"b1(map)": {
"hi there": "hi"
}
}
}
对于有很多语言的大型项目,最好是在配置文件中指定它们。
# 配置
maps: # 应用于所有语言!
- a
- b.b1
现在可以通过键访问译文:
String a = t.a['hello world']; // "hello"
String b = t.b.b0; // "hey"
String c = t.b.b1['hi there']; // "hi"
➤ 动态键 / 平铺 Map
更普遍的方案是 Maps。 所有的译文可通过一维的 Map 使用。
它是开箱即用的。无需配置。
可通过设置 flat_map: false 全局禁用。
String a = t['myPath.anotherPath'];
String b = t['myPath.anotherPath.3']; // 使用数组的下标
String c = t['myPath.anotherPath'](name: 'Tom'); // 使用参数
➤ 修饰符
有多种修饰符用于进一步的调整。
可如下用逗号绑定多个修饰符:
{
"apple(plural, param=appleCount, rich)": {
"one": "I have $appleCount apple.",
"other": "I have $appleCount apples."
}
}
可用的修饰符:
| 修饰符 | 意义 | 可用于 |
|---|---|---|
(rich) | 这是一个 RichText | 叶子、 Map ( 复数 / Context) |
(map) | 这是一个 Map / dictionary (and not a class). | Map |
(plural) | 这是复数 (类型: cardinal) | Map |
(cardinal) | 这是复数 (类型: cardinal) | Map |
(ordinal) | 这是复数 (类型: ordinal) | Map |
(context=<Context Type>) | 这是类型 <Context Type>的上下文 | Map |
(param=<Param Name>) | 带有参数 <Param Name> | Map (复数 / Context) |
复杂特性
➤ 链接译文
可以将一个译文和另一个译文链接起来。
添加前缀 @: ,之后紧跟着译文的键。
{
"fields": {
"name": "my name is {firstName}",
"age": "I am {age} years old"
},
"introduce": "Hello, @:fields.name and @:fields.age"
}
String s = t.introduce(firstName: 'Tom', age: 27); // Hello, my name is Tom and I am 27 years old.
RichText 也能包含链接!但是只有 RichText 能链接到 RichText 。
➤ 复数
该库使用这里定义的概念。
有些语言是直接支持的。查看 这里。
复数通过以下的关键字检测: zero、 one、 two、 few、 many、 other。
// 文件: strings.i18n.json
{
"someKey": {
"apple": {
"one": "I have $count apple.",
"other": "I have $count apples."
}
}
}
String a = t.someKey.apple(count: 1); // 我有一个苹果。
String b = t.someKey.apple(count: 2); // 我有两个苹果。
默认情况下,检测到的复数是基数。
要指定复数,需要添加 (ordinal) 修饰符。
// 文件: strings.i18n.json
{
"someKey": {
"apple(cardinal)": {
// cardinal
"one": "I have $n apple.",
"other": "I have $n apples."
},
"place(ordinal)": {
// ordinal (rarely used)
"one": "${n}st place.",
"two": "${n}nd place.",
"few": "${n}rd place.",
"other": "${n}th place."
}
}
}
也可以在全局配置里指定所有的复数形式。
# 配置
pluralization: # 应用于所有语言!
auto: off
cardinal:
- someKey.apple
ordinal:
- someKey.place
如果你使用的语言不被支持,你必须提供自定义的复数解析器:
// 在调用复数字符串之前添加该部分代码。否则会抛出异常。
// 不需要同时指定
LocaleSettings.setPluralResolver(
language: 'en',
cardinalResolver: (num n, {String? zero, String? one, String? two, String? few, String? many, String? other}) {
if (n == 0)
return zero ?? other!;
if (n == 1)
return one ?? other!;
return other!;
},
ordinalResolver: (num n, {String? zero, String? one, String? two, String? few, String? many, String? other}) {
if (n % 10 == 1 && n % 100 != 11)
return one ?? other!;
if (n % 10 == 2 && n % 100 != 12)
return two ?? other!;
if (n % 10 == 3 && n % 100 != 13)
return few ?? other!;
return other!;
},
);
默认情况下,参数名是 n 。可以添加修饰符来修改。
{
"someKey": {
"apple(param=appleCount)": {
"one": "I have one apple.",
"other": "I have multiple apples."
}
}
}
String a = t.someKey.apple(appleCount: 2); // 注意用 'appleCount' 替换了 'n'
也可以通过 pluralization/default_parameter 设置全局的默认参数。
➤ 自定义 context / 枚举
可以利用自定义 context 区别男性和女性的格式(或其它枚举)。
// 文件: strings.i18n.json
{
"greet": {
"male": "Hello Mr $name",
"female": "Hello Ms $name"
}
}
# 配置
contexts:
GenderContext:
enum:
- male
- female
UserType:
enum:
- user
- admin
String a = t.greet(name: 'Maria', context: GenderContext.female);
自动检测默认是开启的。可以关闭自动检测。这会加速编译时间。
# 配置
contexts:
GenderContext:
enum:
- male
- female
paths: # 只有这些路径会被考虑到
- my.path.to.greet
相对于多元化,你 必须 提供所有格式。可以将其折叠节省空间。
{
"greet": {
"male,female": "Hello $name"
}
}
和复数相似,参数名默认是 context 。可以添加修饰符来修改。
{
"greet(param=gender)": {
"male": "Hello Mr",
"female": "Hello Ms"
}
}
String a = t.greet(gender: GenderContext.female); // 注意用 'gender' 代替了 'context'
、、或者全局设置:
# 配置
contexts:
UserType:
enum:
- user
- admin
default_parameter: type # 默认: "context"
You already have existing enums? You can disable enum generation and import them instead:
已有存在的枚举?可以禁用枚举生成器并导入它们:
# 配置
imports:
- 'package:my_package/path_to_enum.dart' # 定义你的枚举的位置
contexts:
UserType:
enum:
- user
- admin
generate_enum: false # 关闭枚举生成器
➤ Locale Stream
如果想要跟踪 Locale 的改变,请使用 LocaleSettings.getLocaleStream 。
LocaleSettings.getLocaleStream().listen((event) {
print('locale changed: $event');
});
➤ 接口
通常多个对象会有相同的属性。 可为它们创建共同的父类。
{
"onboarding": {
"whatsNew": {
"v2": {
"title": "New in 2.0",
"rows": [
"Add sync"
]
},
"v3": {
"title": "New in 3.0",
"rows": [
"New game modes",
"And a lot more!"
]
}
}
}
}
这里我们知道 whatsNew 里的所有对象都有相同的属性。
我们将这些对象命名为 ChangeData 。
# 配置
interfaces:
ChangeData: onboarding.whatsNew.*
这会创建下面的 mixin :
mixin ChangeData {
String get title;
List<String> get rows;
}
现在可以使用多态访问这些字段:
// 修改前: 不使用接口
void myOldFunction(dynamic changes) {
String title = changes.title; // 类型不安全!
List<String> rows = changes.rows; // 容易出现拼写错误
}
// 修改后: 使用接口
void myFunction(ChangeData changes) {
String title = changes.title;
List<String> rows = changes.rows;
}
void main() {
myFunction(t.onboarding.whatsNew.v2);
myFunction(t.onboarding.whatsNew.v3);
}
可以自定义属性,并使用不同的节点选择器。
查看 完整文章。
➤ 语言枚举
类型安全是该库的主要优点。 不会有拼写错误。尽情享受语言切换!
// 该枚举是自动生成的
enum AppLocale {
en,
fr,
zhCn,
}
// 扩展方法
Locale locale = AppLocale.en.flutterLocale; // 本地 Flutter 语言
String tag = AppLocale.en.languageTag; // 字符串标签(例: en-US)
final t = AppLocale.en.translations; // 获取某个语言的译文
➤ 译文覆写
You may want to update translations dynamically (e.g. via backend server over network).
你可能想要动态更新译文(例:通过后台服务器或网络)。 你可能只更新存在的译文。 如下设定配置:
# 配置
translation_overrides: true
示例:
// 覆写
LocaleSettings.overrideTranslations(
locale: AppLocale.en,
fileType: FileType.yaml,
content: r'''
onboarding
title: 'Welcome {name}'
'''
);
// 访问
String a = t.onboarding.title(name: 'Tom'); // "Welcome Tom"
➤ 依赖注入
不喜欢包含 LocaleSettings 的方案?
那可以使用自己的依赖注入方案!
只需要创建自定义译文实例,不依赖 LocaleSettings 或有其它副作用。
首先,如下进行设置:
# 配置
locale_handling: false # remove unused t variable, LocaleSettings, etc.
translation_class_visibility: public
使用 riverpod 库的示例:
final english = AppLocale.en.build(cardinalResolver: myEnResolver);
final german = AppLocale.de.build(cardinalResolver: myDeResolver);
final translationProvider = StateProvider<StringsEn>((ref) => german); // 设置它
// 访问当前实例
final t = ref.watch(translationProvider);
String a = t.welcome.title;
查看 完整文章。
结构化特性
➤ 命名空间
可以将译文分割为多个文件。每个文件代表一个命名空间。
使用单文件时,该特性默认不可用。必须启用它。
# 配置
namespaces: true # 启用该特性
output_directory: lib/i18n # 可选
output_file_name: translations.g.dart # 设置文件名(强制)
创建两个命名空间,分别叫做 widgets 和 dialogs。
<namespace>_<locale?>.<extension>
i18n/
└── widgets.i18n.json
└── widgets_fr.i18n.json
└── dialogs.i18n.json
└── dialogs_fr.i18n.json
也可以使用不同的目录。只会关注文件名!
i18n/
└── widgets/
└── widgets.i18n.json
└── widgets_fr.i18n.json
└── dialogs/
└── dialogs.i18n.json
└── dialogs_fr.i18n.json
i18n/
└── en/
└── widgets.i18n.json
└── dialogs.i18n.json
└── fr/
└── widgets_fr.i18n.json
└── dialogs.i18n.json <-- 目录语言会被使用
现在访问译文:
// t.<namespace>.<path>
String a = t.widgets.welcomeCard.title;
String b = t.dialogs.logout.title;
➤ 输出格式
默认情况下,会生成单个文件 .g.dart 。
可以将该文件分割成多个以改善可读性和 IDE 的性能。
# 配置
output_file_name: translations.g.dart
output_format: multiple_files # set this
这会生成以下文件:
lib/
└── i18n/
└── translations.g.dart <-- 主文件
└── translations_en.g.dart <-- 译文类
└── translations_de.g.dart <-- 译文类
└── ...
└── translations_map.g.dart <-- 平铺 Map 中存储的译文
只需要导入主文件!
➤ 压缩 CSV
正常情况下,会为每个语言创建新的 CSV 文件: strings.i18n.csv, strings_fr.i18n.csv, 等。
也可以将多个语言合并到一个 CSV 文件中! 要这样做,至少需要3列。 第一行包含语言名。 该库会检测该行,所以无需配置。
支持注释。(查看 注释)
,locale_0 ,locale_1 , ... ,locale_n
key_0,string_00,string_01, ... ,string_0n
key_1,string_10,string_11, ... ,string_1n
...
key_m,string_m0,string_m1, ... ,string_mn
示例:
key,en,de-DE
welcome.title,Welcome $name,Willkommen $name
welcome.button,Start,Start
assets/
└── i18n/
└── strings.i18n.csv <-- contains all locales
其它特性
➤ 回退
默认情况下,必须为所有语言提供译文。否则无法编译。
为了对应快速开发,可以关闭该特性。 缺失的译文会回退到基础语言。
# 配置
base_locale: en
fallback_strategy: base_locale # 添加该行
// English 英语
{
"hello": "Hello",
"bye": "Bye"
}
// French 法语
{
"hello": "Salut",
// "bye" 缺失,回退到英语版本
}
➤ 注释
可以在译文文件中添加注释。
JSON
所有以 @ 开头的键会被忽略。
如果有一个 @key 的键匹配一个现有的键,那其内容会作为注释。
{
"@@locale": "en", // 完全忽略
"mainScreen": {
"button": "Submit",
// 不会作为译文,会提取作为注释
"@button": "The submit button shown at the bottom",
// ARB 格式也可用, description 会提取作为注释
"@button2": {
"context": "HomePage",
"description": "The submit button shown at the bottom"
},
}
}
YAML
现在,不会生成解析和注释。
mainScreen:
button: Submit # submit 按钮在底部显示
CSV
带括号的列如 (my_column) 会被忽略。
第一个带括号的列的值会作为注释。
key,(comment),en,de,(ignored comment)
mainScreen.button,The submit button shown at the bottom,Submit,Bestätigen,fully ignored
mainScreen.content,,Content,Inhalt,
生成的文件
/// submit 按钮在底部显示
String get button => 'Submit';
➤ 重新包装
默认情况下,不会有转换。
可以指定 key_case 、 key_map_case 或 param_case 来改变。
可能的情况是: camel(驼峰)、 snake(蛇形) 和 pascal 。
{
"must_be_camel_case": "The parameter is in {snakeCase}",
"my_map": {
"this_should_be_in_pascal": "hi"
}
}
# 配置
key_case: camel
key_map_case: pascal
param_case: snake
maps:
- myMap # 所有的路径必须相应地大小写
String a = t.mustBeCamelCase(snake_case: 'nice');
String b = t.myMap['ThisShouldBeInPascal'];
➤ 仅用于Dart
也可以在非 Flutter 环境中使用该库。
# 配置
flutter_integration: false # 设置该配置
工具
➤ 主要命令
主要命令是从译文资源生成 dart 文件。
flutter pub run slang
➤ 迁移
有一些工具,使从其它 i18n 方案迁移更容易。
通常的迁移语法:
flutter pub run slang:migrate <type> <source> <destination>
ARB
转换 ARB 文件兼容 JSON 格式。所有的 description (描述)都会保持。
flutter pub run slang:migrate arb source.arb destination.json
ARB 输入:
{
"@@locale": "en_US",
"@@context": "HomePage",
"title_bar": "My Cool Home",
"@title_bar": {
"type": "text",
"context": "HomePage",
"description": "Page title."
},
"FOO_123": "Your pending cost is {COST}",
"foo456": "Hello {0}",
"pageHomeInboxCount" : "{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}}",
"@pageHomeInboxCount" : {
"placeholders": {
"count": {}
}
}
}
JSON 结果:
{
"@@locale": "en_US",
"@@context": "HomePage",
"title": {
"bar": "My Cool Home",
"@bar": "Page title."
},
"foo123": "Your pending cost is {cost}",
"foo456": "Hello {arg0}",
"page": {
"home": {
"inbox": {
"count(count)": {
"zero": "You have no new messages",
"one": "You have 1 new message",
"other": "You have {count} new messages"
}
}
}
}
}
➤ 统计数据
下面的命令可快速获取单词或字符等的个数。
flutter pub run slang:stats
控制台输出的示例:
[en]
- 9 keys (including intermediate keys)
- 6 translations (leaves only)
- 15 words
- 82 characters (ex. [,.?!'¿¡])
➤ 自动重新编译
可使该库自动重新编译。
build_runner 的监视功能 不再 维护。
flutter pub run slang:watch
常见问题
能在 asset 目录下编写 json 文件吗?
可以。在 build.yaml 中指定 input_directory 和 output_directory。
targets:
$default:
sources:
- "custom-directory/**" # 可选;只有 assets/* 和 lib/* 会被 build_runner 扫描
builders:
slang_build_runner:
options:
input_directory: assets/i18n
output_directory: lib/i18n
或者在 slang.yaml 中指定:
input_directory: assets/i18n
output_directory: lib/i18n
CSV 文件不能正确解析
注意行结束符必须是 CRLF 。
能够忽略译文或者使用基础语言中的译文吗?
可以。在 build.yaml 设置 fallback_strategy: base_locale 。
现在你可以忽略第二语言的译文。 缺失的译文会自动回退到基础语言。
是否可以阻止“内置”时间戳更新?
不能,但可以完全禁用时间戳。
在 build.yaml 设置 timestamp: false 。
为什么 setLocale 不起作用?
大多数情况下,是忘了调用 setState 。
更优雅的方案是使用 TranslationProvider(child: MyApp()) ,然后用 final t = Translations.of(context) 获得使用译文的变量。
setLocale 时,它会自动为所有影响的组件重新刷新。
自己的复数解析器没有指定?
_missingPluralResolver 会因为没有为指定语言添加 LocaleSettings.setPluralResolver 抛出异常。
查看 复数。
复数 / context 检测的机制是?
可以使该库检测复数或 context 。
对于复数,如果任意 json 节点中有zero 、 one 、 two 、 few 、 many 或 other 作为子节点。
只要检测到有未知项目,那该 json 节点就 不是 多元化的。
{
"fake": {
"one": "One apple",
"two": "Two apples",
"three": "Three apples" // 未知的关键字 'three' , 'fake' 不是多元化的。
}
}
对于 context ,所有的检举值都必须存在。
如何在一句话中使用多个复数?
可能要使用链接译文来解决该问题。
{
"apples(appleCount)": {
"one": "one apple",
"other": "{appleCount} apples"
},
"bananas(bananaCount)": {
"one": "one banana",
"other": "{bananaCount} bananas"
},
"sentence": "I have @:apples and @:bananas"
}
String a = t.sentence(appleCount: 1, bananaCount: 2); // 两个不同的复数参数!
AppLocale.en.translations 和 AppLocale.en.build() 的区别是什么?
AppLocale.<locale>.translations 的复数解析器需要通过LocaleSettings.setPluralResolver 设置。
因此,调用 LocaleSettings 对于 AppLocale.<locale>.translations 有副作用。
当调用 AppLocale.<locale>.build() 时,没有副作用。
进一步讲,第一个方法返回该库管理的实例,第二个方法总是返回一个新实例。
进一步阅读
深入
向导
可自由扩展该列表 :)
许可证
MIT 许可
详细内容参考原文。