[译]Flutter多语言国际化库slang

1,462 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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_runnerbuild.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_localeString默认 json 的语言en
fallback_strategynonebase_locale处理缺失的译文 (i)none
input_directoryString输入目录的路径null
input_file_patternString输入文件的模式(文件名格式),必须以 .json 、 .yaml 或 .csv 结尾.i18n.json
output_directoryString输出目录的路径null
output_file_nameString输出文件名null
output_formatsingle_filemultiple_files切割的输出文件 (i)single_file
locale_handlingBoolean生成语言的处理逻辑 (i)true
flutter_integrationBoolean生成 flutter 特性 (i)true
namespacesBoolean分割输入文件 (i)false
translate_varString翻译变量名t
enum_nameString枚举名AppLocale
translation_class_visibilityprivatepublic类的可见性 visibilityprivate
key_casenullcamelpascalsnake转换键(可选) (i)null
key_map_casenullcamelpascalsnake从 Map 中转换 key (可选) (i)null
param_casenullcamelpascalsnake转换参数 (可选) (i)null
string_interpolationdartbracesdouble_braces字符串插值模式 (i)dart
flat_mapBoolean生成单层 Map (i)true
translation_overridesBoolean允许覆写译文 (i)false
timestampBoolean写入 "Built on" 时间戳true
mapsList<String>应该通过键访问的实体(Map) (i)[]
pluralization/autooffcardinalordinal自动检测复数 (i)cardinal
pluralization/default_parameterString默认的复数参数 (i)n
pluralization/cardinalList<String>有基数内容的实体[]
pluralization/ordinalList<String>有序数的实体[]
<context>/enumList<String>context 格式 (i)无默认值
<context>/autoBoolean自动检测 contexttrue
<context>/pathsList<String>使用该 context 的实体[]
<context>/default_parameterString默认的参数名context
<context>/generate_enumBoolean生成枚举true
children of interfacesPairs of Alias:Path别名接口 (i)null
importsList<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_casekey_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() 时,没有副作用。

进一步讲,第一个方法返回该库管理的实例,第二个方法总是返回一个新实例。

进一步阅读

深入

接口

依赖注入

向导

Medium (英语)

Qiita (日语)

Youtube (韩语)

可自由扩展该列表 :)

许可证

MIT 许可

详细内容参考原文。