本地化 (i18n)

Finch 内置了支持多语言的 i18n 系统。语言字符串可以存储在 JSON 文件中,也可以直接在 Dart 中定义。

配置

在 FinchConfigs 中设置语言选项:

FinchConfigs configs = FinchConfigs(
  // 存放语言 JSON 文件的目录路径
  languagePath: pathTo(env.get('LANGUAGE_PATH', './lib/languages')),

  // 使用 JSON 文件(LanguageSource.json)还是 Dart map(LanguageSource.dart)
  languageSource: LanguageSource.json,

  // 如果使用 LanguageSource.dart,请在此处提供 map:
  // dartLanguages: languageDart,
);

选项 1:JSON 文件

在你的 languagePath 目录中为每种语言创建一个 JSON 文件,文件名即为语言代码:

lib/languages/en.json

{
  "dir": "ltr",
  "logo.title": "My App",
  "greeting": "Hello, World!",
  "example.params": "My name is {name}, my age is {age}",
  "example.params.arr": "My name is {0}, my age is {1}"
}

lib/languages/fa.json

{
  "dir": "rtl",
  "logo.title": "برنامه من",
  "greeting": "سلام دنیا!",
  "example.params": "نام من {name} است، سن من {age} سال است"
}

这里的 dir 键(ltr / rtl)和其他键一样,只是一个普通的翻译键——应该在模板中使用 {{ $t('dir') }} 来读取它(正如 Templates 示例中所做的那样),而不是 {{ $e.dir }}。$e.dir 查找的是另一个命名方式不同的键——相关区别请参阅 Template Events。

选项 2:Dart Map

将语言定义为 Dart 的 Map<String, Map<String, String>>,并让 FinchConfigs 指向它:

// lib/languages/language_dart.g.dart(或任意文件名)
const languageDart = <String, Map<String, String>>{
  'en': {
    'dir': 'ltr',
    'logo.title': 'My App',
    'greeting': 'Hello, World!',
    'example.params': 'My name is {name}, my age is {age}',
  },
  'fa': {
    'dir': 'rtl',
    'logo.title': 'برنامه من',
    'greeting': 'سلام دنیا!',
    'example.params': 'نام من {name} است، سن من {age} سال است',
  },
};
FinchConfigs configs = FinchConfigs(
  languageSource: LanguageSource.dart,
  dartLanguages: languageDart,
);

finch serve 监听器可以自动将 JSON 文件转换为 Dart map。生成的文件会写入 language_dart.g.dart。

Translating in Controllers

对任意键字符串使用 .tr 扩展。.write() 会渲染出最终的翻译字符串:

// 简单翻译
String text = 'logo.title'.tr.write();

// 使用命名参数
String text = 'example.params'.tr.write({'name': 'Alice', 'age': 30});

// 使用位置参数(数组)—— 并不存在单纯的 `.writeArr()`;
// 需要将当前语言显式传给 `writeByLangArr`
String text = 'example.params.arr'.tr.writeByLangArr(rq.getLanguage(), ['Alice', 30]);

将翻译后的字符串传递给模板:

rq.addParams({
  'greeting': 'greeting'.tr.write(),
  'userLine': 'example.params'.tr.write({'name': 'Alice', 'age': 30}),
});
return rq.renderView(path: 'pages/home');

在模板中翻译

使用 {{ $t('key') }} 进行内联翻译:

<h1>{{ $t('logo.title') }}</h1>
<p>{{ $t('example.params', {'name': user.name, 'age': user.age}) }}</p>
<p>{{ $t('example.params.arr', ['Alice', 30]) }}</p>

语言切换

语言按以下顺序确定:

  1. 第一个路径段,如果它匹配某个已知的语言代码(例如 /fa/home)
  2. API 接口上的 lang 数据字段
  3. language cookie
  4. language 会话(session)键
  5. 配置中的默认值

无论最终解析出的值是什么,都会始终根据 FinchConfigs.languages 进行校验;如果它不属于已配置的语言之一(例如无效或过期的 cookie),Finch 会回退使用 'en'。

在控制器中修改语言:

rq.changeLanguege('fa');

在模板中生成一个用于切换语言的 URL:

<a href="{{ $e.urlToLanguage('fa') }}">فارسی</a>
<a href="{{ $e.urlToLanguage('en') }}">English</a>

模板中的可用语言

{% for lang in $e.langs %}
  <a href="{{ $e.urlToLanguage(lang.code) }}">{{ lang.label }}</a>
{% endfor %}

每个条目的结构为:{ code: 'en', label: 'English', contry: 'United States' }——注意,除非你已经添加了 Template Events 中所述的 language.<code>_label/language.<code>_contry 键,否则 label/contry 字段会是空的或无法解析。

TString —— 翻译对象

TString 会包装一个键,让你可以延迟进行翻译:

var ts = TString('example.params');
rq.addParam('exampleTString', ts.write());

// 或者使用 .tr 简写形式:
rq.addParam('examplePathString', 'example.path'.tr.write());

位置(数组)参数

翻译字符串除了可以使用命名的 {name} 占位符外,还可以使用位置占位符 {0}、{1}……:

{
  "example.params.arr": "My name is {0}, my age is {1}"
}

在模板中,通过给 $t 传入一个列表参数来填充它们:

<p>{{ $t('example.params.arr', ['Alexandre', 30]) }}</p>

或者在 Dart 中使用 writeByLangArr(并不存在单纯的 .writeArr() 简写形式——参见上文的 Translating in Controllers):

rq.renderString(text: 'example.params.arr'.tr.writeByLangArr(rq.getLanguage(), ['Alexandre', 30]));