国际化

国际化和本地化

国际化和本地化的目的是帮助你为多个地区和语言创建 Web 应用程序。@adonisjs/i18n 包提供了对 i18n(Internationalization 的缩写)的支持。

  • 本地化(Localization) 是将应用程序的文本翻译成多种语言的过程。你必须为每种语言编写副本,并在 Edge 模板、验证错误消息中引用它们,或者直接使用 i18n API。

  • 国际化(Internationalization) 是根据特定地区或国家格式化 日期时间数字 等值的过程。

安装

使用以下命令安装并配置该包:

node ace add @adonisjs/i18n
  1. 使用检测到的包管理器安装 @adonisjs/i18n 包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
    providers: [
    // ...其他提供者
    () => import('@adonisjs/i18n/i18n_provider')
    ]
    }
  3. 创建 config/i18n.ts 文件。

  4. middleware 目录中创建 detect_user_locale_middleware

  5. start/kernel.ts 文件中注册以下中间件。

    router.use([
    () => import('#middleware/detect_user_locale_middleware')
    ])

配置

i18n 包的配置存储在 config/i18n.ts 文件中。

另请参阅:Config stub

import app from '@adonisjs/core/services/app'
import { defineConfig, formatters, loaders } from '@adonisjs/i18n'
const i18nConfig = defineConfig({
defaultLocale: 'en',
formatter: formatters.icu(),
loaders: [
loaders.fs({
location: app.languageFilesPath()
})
],
})
export default i18nConfig

formatter

定义用于存储翻译的格式。AdonisJS 支持 ICU 消息格式

ICU 消息格式是一种被广泛接受的标准,许多翻译服务(如 Crowdin 和 Lokalise)都支持它。

此外,你可以 添加自定义消息格式化器

defaultLocale

应用程序的默认区域设置。当你的应用程序不支持用户语言时,翻译和值格式化将回退到此区域设置。

fallbackLocales

一个键值对,定义了一组区域设置及其备用区域设置。例如,如果你的应用程序支持西班牙语,你可以将其定义为加泰罗尼亚语的备用语言。

export default defineConfig({
formatter: formatters.icu(),
defaultLocale: 'en',
fallbackLocales: {
ca: 'es' // 当用户说加泰罗尼亚语时显示西班牙语内容
}
})

supportedLocales

你的应用程序支持的区域设置数组。

export default defineConfig({
formatter: formatters.icu(),
defaultLocale: 'en',
supportedLocales: ['en', 'fr', 'it']
})

如果你未定义此值,我们将从翻译中推断 supportedLocales。例如,如果你定义了英语、法语和西班牙语的翻译,则 supportedLocales 的值将为 ['en', 'es', 'fr']

loaders

用于加载翻译的加载器集合。默认情况下,我们只支持文件系统加载器。但是,你可以 添加自定义加载器

存储翻译

翻译存储在 resources/lang 目录中,你必须根据 IETF 语言标签 格式为每种语言创建一个子目录。例如:

resources
├── lang
│ ├── en
│ └── fr

你可以通过创建带有地区代码的子目录来为特定地区定义翻译。在以下示例中,我们为 英语(全球)英语(美国)英语(英国) 定义了不同的翻译。

当你在特定地区翻译集中缺少翻译时,AdonisJS 将自动回退到 英语(全球)

另请参阅:ISO 语言代码

resources
├── lang
│ ├── en
│ ├── en-us
│ ├── en-uk

文件格式

翻译必须存储在 .json.yaml 文件中。你可以创建嵌套的目录结构以更好地组织文件。

resources
├── lang
│ ├── en
│ │ └── messages.json
│ └── fr
│ └── messages.json

翻译必须按照 ICU 消息语法 进行格式化。

resources/lang/en/messages.json
{
"greeting": "Hello world"
}
resources/lang/fr/messages.json
{
"greeting": "Bonjour le monde"
}

解析翻译

在查找和格式化翻译之前,你必须使用 i18nManager.locale 方法为特定区域设置创建一个 I18n 类 的实例。

import i18nManager from '@adonisjs/i18n/services/main'
// 英语的 I18n 实例
const en = i18nManager.locale('en')
// 法语的 I18n 实例
const fr = i18nManager.locale('fr')

一旦你获得了 I18n 类的实例,你可以使用 .t 方法来格式化翻译。

const i18n = i18nManager.locale('en')
i18n.t('messages.greeting') // Hello world
const i18n = i18nManager.locale('fr')
i18n.t('messages.greeting') // Bonjour le monde

备用区域设置

每个实例都基于 config.fallbackLocales 集合预配置了一个备用语言。当主要语言缺少翻译时,会使用备用语言。

export default defineConfig({
fallbackLocales: {
'de-CH': 'de',
'fr-CH': 'fr'
}
})
const i18n = i18nManager.locale('de-CH')
i18n.fallbackLocale // de(使用备用集合)
const i18n = i18nManager.locale('fr-CH')
i18n.fallbackLocale // fr(使用备用集合)
const i18n = i18nManager.locale('en')
i18n.fallbackLocale // en(使用默认区域设置)

缺少的翻译

如果主要区域设置和备用区域设置中都缺少翻译,.t 方法将返回格式如下的错误字符串。

const i18n = i18nManager.locale('en')
i18n.t('messages.hero_title')
// translation missing: en, messages.hero_title

你可以通过将备选值作为第二个参数来替换此消息为其他消息或空字符串。

const fallbackValue = ''
i18n.t('messages.hero_title', fallbackValue)
// 输出: ''

你还可以通过配置文件全局计算备选值。fallback 方法接收翻译路径作为第一个参数,接收区域设置代码作为第二个参数。确保始终返回字符串值。

import { defineConfig } from '@adonisjs/i18n'
export default defineConfig({
fallback: (identifier, locale) => {
return ''
},
})

在 HTTP 请求期间检测用户区域设置

在初始设置期间,我们在 ./app/middleware 目录中创建一个 detect_user_locale_middleware.ts 文件。中间件执行以下操作。

  • 使用 Accept-language header 检测请求的区域设置。

  • 为请求区域设置创建一个 I18n 类的实例,并使用 HTTP Context 将其与请求的其余部分共享。

  • 作为全局 i18n 属性,与 Edge 模板共享同一实例。

  • 最后,挂钩到 Request validator,并使用翻译文件提供验证消息。

如果此中间件处于活动状态,你可以在控制器和 Edge 模板中按如下方式翻译消息。

import { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({ i18n, session }: HttpContext) {
session.flash('success', {
message: i18n.t('post.created')
})
}
}
<h1> {{ t('messages.heroTitle') }} </h1>

更改用户语言检测代码

由于 detect_user_locale_middleware 是你应用程序代码库的一部分,你可以修改 getRequestLocale 方法,并使用自定义逻辑来查找用户语言。

翻译验证消息

detect_user_locale_middleware 挂钩到 Request validator,并使用翻译文件提供验证消息。

export default class DetectUserLocaleMiddleware {
static {
RequestValidator.messagesProvider = (ctx) => {
return ctx.i18n.createMessagesProvider()
}
}
}

翻译必须存储在 validator.json 文件中的 shared 键下。可以为验证规则或 field + rule 组合定义验证消息。

resources/lang/en/validator.json
{
"shared": {
"fields": {
"first_name": "first name"
},
"messages": {
"required": "Enter {field}",
"username.required": "Choose a username for your account",
"email": "The email must be valid"
}
}
}
resources/lang/fr/validator.json
{
"shared": {
"fields": {
"first_name": "Prénom"
},
"messages": {
"required": "Remplisser le champ {field}",
"username.required": "Choissisez un nom d'utilisateur pour votre compte",
"email": "L'email doit être valide"
}
}
}

直接使用 VineJS 进行翻译

在 HTTP 请求期间,detect_user_locale_middleware 挂钩到请求验证器,并注册一个 custom messages provider,以从翻译文件中查找验证错误。

但是,如果你在 HTTP 请求之外使用 VineJS,例如在 Ace 命令或队列作业中,则在调用 validator.validate 方法时,必须显式注册一个自定义消息提供者。

import { createJobValidator } from '#validators/jobs'
import i18nManager from '@adonisjs/i18n/services/main'
/**
* 获取特定区域设置的 i18n 实例
*/
const i18n = i18nManager.locale('fr')
await createJobValidator.validate(data, {
/**
* 注册用于翻译的
* 消息提供者
*/
messagesProvider: i18n.createMessagesProvider()
})

ICU 消息格式

插值

ICU 消息语法使用单个大括号来引用动态值。例如:

ICU 消息语法 不支持嵌套数据集,因此,在插值时,你只能从平面对象中访问属性。

{
"greeting": "Hello { username }"
}
{{ t('messages.greeting', { username: 'Virk' }) }}

你还可以在消息中编写 HTML。但是,在 Edge 模板中,请使用三个 大括号 来渲染 HTML,而不对其进行转义。

{
"greeting": "<p> Hello { username } </p>"
}
{{{ t('messages.greeting', { username: 'Virk' }) }}}

数字格式

你可以在翻译消息中使用 {key, type, format} 语法来格式化数值。在以下示例中:

  • amount 是运行时值。
  • number 是格式化类型。
  • ::currency/USD 是带有 数字骨架 的货币格式。
{
"bagel_price": "The price of this bagel is {amount, number, ::currency/USD}"
}
{{ t('bagel_price', { amount: 2.49 }) }}
The price of this bagel is $2.49

以下是使用 number 格式与不同格式样式和数字骨架的示例。

Length of the pole: {price, number, ::measure-unit/length-meter}
Account balance: {price, number, ::currency/USD compact-long}

日期/时间格式

你可以使用 {key, type, format} 语法来格式化 Date 实例或 luxon DateTime 实例。在以下示例中:

  • expectedDate 是运行时值。
  • date 是格式化类型。
  • medium 是日期格式。
{
"shipment_update": "Your package will arrive on {expectedDate, date, medium}"
}
{{ t('shipment_update', { expectedDate: luxonDateTime }) }}
Your package will arrive on Oct 16, 2023

你可以使用 time 格式将值格式化为时间。

{
"appointment": "You have an appointment today at {appointmentAt, time, ::h:m a}"
}
You have an appointment today at 2:48 PM

ICU 提供了 多种模式 来定制日期时间格式。然而,并非所有这些模式都可通过 ECMA402 的 Intl API 使用。因此,我们仅支持以下模式。

符号描述
G年代标识符
y
M年中的月份
L年中独立月份
d月中的天数
E一周中的天数
e本地一周中的天数 e..eee 不支持
c本地独立一周中的天数 c..ccc 不支持
a上午/下午标记
h小时 [1-12]
H小时 [0-23]
K小时 [0-11]
k小时 [1-24]
m分钟
s
z时区

复数规则

ICU 消息语法支持在消息中定义复数规则。例如:

在以下示例中,我们使用 YAML 而不是 JSON,因为在 YAML 中编写多行文本更容易。

cart_summary:
"You have {itemsCount, plural,
=0 {no items}
one {1 item}
other {# items}
} in your cart"
{{ t('messages.cart_summary', { itemsCount: 1 }) }}
你的购物车中有 1 件商品

# 是一个特殊标记,用作数值的占位符。它将格式化为 {key, number}

{{ t('messages.cart_summary', { itemsCount: 1000 }) }}
{{-- Output --}}
{{-- 你的购物车中有 1,000 件商品 --}}

复数规则使用 {key, plural, matches} 语法。matches 是与以下复数类别之一匹配的字面值。

类别描述
zero该类别用于语法专门针对零个项目的语言。(例如阿拉伯语和拉脱维亚语)
one该类别用于语法专门针对一个项目的语言。许多语言使用这个复数类别,但并非所有语言都如此。(许多流行的亚洲语言,如中文和日语,不使用此类别。)
two该类别用于语法专门针对两个项目的语言。(例如阿拉伯语和威尔士语。)
few该类别用于语法专门针对少量项目的语言。对于某些语言,这适用于 2-4 个项目,对于某些语言适用于 3-10 个项目,而其他语言则有更复杂的规则。
many该类别用于语法专门针对较大数量项目的语言。(例如阿拉伯语、波兰语和俄语。)
other如果值不匹配其他复数类别,则使用该类别。请注意,对于具有简单“单数”与“复数”二分法的语言(如英语),此类别用于“复数”。
=value这用于匹配特定值,而不考虑当前语言环境的复数类别。

表格内容参考自 formatjs.io

选择

select 格式允许你通过将值与多个选项之一匹配来选择输出。撰写性别特定的文本是 select 格式的一个绝佳示例。

Yaml
auto_reply:
"{gender, select,
male {He}
female {She}
other {They}
} will respond shortly."
{{ t('messages.auto_reply', { gender: 'female' }) }}
她很快就会回复。

选择序数

select ordinal 格式允许你根据序数复数化规则选择输出。该格式与 select 格式相似。但是,值被映射到序数复数类别。

anniversary_greeting:
"It's my {years, selectordinal,
one {#st}
two {#nd}
few {#rd}
other {#th}
} anniversary"
{{ t('messages.anniversary_greeting', { years: 2 }) }}
这是我的第二个周年纪念日

选择序数格式使用 {key, selectordinal, matches} 语法。匹配项是字面值,并与以下复数类别之一匹配。

类别描述
zero此类别用于专门为零个项目设计的语法。(例如阿拉伯语和拉脱维亚语。)
one此类别用于专门为一项设计的语法。许多语言(但并非所有语言)使用此复数类别。(许多流行的亚洲语言,如中文和日语,不使用此类别。)
two此类别用于专门为两个项目设计的语法。(例如阿拉伯语和威尔士语。)
few此类别用于专门为少量项目设计的语法。在某些语言中,此类别用于2-4个项目,在某些语言中用于3-10个项目,而其他语言则有更复杂的规则。
many此类别用于专门为较大数量的项目设计的语法。(例如阿拉伯语、波兰语和俄语。)
other如果值不匹配其他复数类别,则使用此类别。请注意,对于具有简单的“单数”与“复数”二分法的语言(如英语),此类别用于“复数”。
=value此选项用于匹配特定值,而不考虑当前语言环境的复数类别。

表格内容引用自 formatjs.io

格式化值

以下方法底层使用 Node.js Intl API,但性能更好。查看基准测试

formatNumber

使用 Intl.NumberFormat 类格式化数值。你可以传递以下参数。

  1. 要格式化的值。
  2. 可选的 options 对象
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatNumber(123456.789, {
maximumSignificantDigits: 3
})

formatCurrency

使用 Intl.NumberFormat 类将数值格式化为货币。formatCurrency 方法隐式定义了 style = currency 选项。

import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatCurrency(200, {
currency: 'USD'
})

formatDate

使用 Intl.DateTimeFormat 类格式化日期或 luxon 日期时间对象。你可以传递以下参数。

  1. 要格式化的值。可以是 Date 对象或 luxon DateTime 对象。
  2. 可选的 options 对象
import i18nManager from '@adonisjs/i18n/services/main'
import { DateTime } from 'luxon'
i18nManager
.locale('en')
.formatDate(new Date(), {
dateStyle: 'long'
})
// 格式化 luxon 日期时间实例
i18nManager
.locale('en')
.formatDate(DateTime.local(), {
dateStyle: 'long'
})

formatTime

使用 Intl.DateTimeFormat 类将日期值格式化为时间字符串。formatTime 方法隐式定义了 timeStyle = medium 选项。

import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatTime(new Date())

formatRelativeTime

formatRelativeTime 方法使用 Intl.RelativeTimeFormat 类将值格式化为相对时间表示字符串。该方法接受以下参数。

  • 要格式化的值。
  • 格式化单位。除了 官方支持的单位 外,我们还支持额外的 auto 单位。
  • 可选的 options 对象。
import { DateTime } from 'luxon'
import i18nManager from '@adonisjs/i18n/services/main'
const luxonDate = DateTime.local().plus({ hours: 2 })
i18nManager
.locale('en')
.formatRelativeTime(luxonDate, 'hours')

将单位的值设置为 auto 以显示最佳匹配单位的差异。

const luxonDate = DateTime.local().plus({ hours: 2 })
I18n
.locale('en')
.formatRelativeTime(luxonDate, 'auto')
// In 2 hours 👈
const luxonDate = DateTime.local().plus({ hours: 200 })
I18n
.locale('en')
.formatRelativeTime(luxonDate, 'auto')
// In 8 days 👈

formatPlural

使用 Intl.PluralRules 类查找数字的复数类别。你可以传递以下参数。

  1. 要查找复数类别的数值。
  2. 可选的 options 对象。
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager.i18nManager('en').formatPlural(0)
// other
i18nManager.i18nManager('en').formatPlural(1)
// one
i18nManager.i18nManager('en').formatPlural(2)
// other

formatList

使用 Intl.ListFormat 类将字符串数组格式化为句子。你可以传递以下参数。

  1. 要格式化的值。
  2. 可选的 options 对象。
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatList(['Me', 'myself', 'I'], { type: 'conjunction' })
// Me, myself and I
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatList(['5 hours', '3 minutes'], { type: 'unit' })
// 5 hours, 3 minutes

formatDisplayNames

使用 Intl.DisplayNames 类将 currency(货币)、language(语言)、region(地区)和 calendar(日历)代码格式化为它们的显示名称。你可以传递以下参数。

  1. 要格式化的代码。根据格式化 type(类型)的不同,code 的值 会有所不同。
  2. Options 对象。
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatDisplayNames('INR', { type: 'currency' })
// Indian Rupee
import i18nManager from '@adonisjs/i18n/services/main'
i18nManager
.locale('en')
.formatDisplayNames('en-US', { type: 'language' })
// American English

配置 i18n Ally VSCode 扩展

VSCode 的 i18n Ally 扩展提供了一个优秀的工作流程,用于在代码编辑器中 存储检查引用 翻译。

要使该扩展与 AdonisJS 无缝协作,你必须在项目根目录的 .vscode 目录中创建以下文件。

mkdir .vscode
touch .vscode/i18n-ally-custom-framework.yml
touch .vscode/settings.json

将以下内容复制/粘贴到 settings.json 文件中。

.vscode/settings.json
{
"i18n-ally.localesPaths": [
"resources/lang"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.namespace": true,
"i18n-ally.editor.preferEditor": true,
"i18n-ally.refactor.templates": [
{
"templates": [
"{{ t('{key}'{args}) }}"
],
"include": [
"**/*.edge",
],
},
]
}

将以下内容复制/粘贴到 .vscode/i18n-ally-custom-framework.yml 文件中。

.vscode/i18n-ally-custom-framework.yml
languageIds:
- edge
usageMatchRegex:
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
sortKeys: true

监听缺失翻译事件

你可以监听 i18n:missing:translation 事件,以获取应用程序中缺失翻译的通知。

import emitter from '@adonisjs/core/services/emitter'
emitter.on('i18n:missing:translation', function (event) {
console.log(event.identifier)
console.log(event.hasFallback)
console.log(event.locale)
})

强制重新加载翻译

@adonisjs/i18n 包在应用程序启动时读取翻译文件,并将它们存储在内存中以便快速访问。

然而,如果你在应用程序运行时修改了翻译文件,可以使用 reloadTranslations 方法刷新内存缓存。

import i18nManager from '@adonisjs/i18n/services/main'
await i18nManager.reloadTranslations()

创建自定义翻译加载器

翻译加载器负责从持久存储中加载翻译。我们提供了一个文件系统加载器,并提供了一个 API 来注册自定义加载器。

加载器必须实现 TranslationsLoaderContract 接口,并定义 load 方法,该方法返回一个包含键值对的对象。键是区域设置代码,值是包含翻译列表的扁平对象。

import type {
LoaderFactory,
TranslationsLoaderContract,
} from '@adonisjs/i18n/types'
/**
* 配置的类型
*/
export type DbLoaderConfig = {
connection: string
tableName: string
}
/**
* 加载器实现
*/
export class DbLoader implements TranslationsLoaderContract {
constructor(public config: DbLoaderConfig) {
}
async load() {
return {
en: {
'messages.greeting': 'Hello world',
},
fr: {
'messages.greeting': 'Bonjour le monde',
}
}
}
}
/**
* 工厂函数,用于在配置文件中引用加载器。
*/
export function dbLoader(config: DbLoaderConfig): LoaderFactory {
return () => {
return new DbLoader(config)
}
}

在上面的代码示例中,我们导出了以下值。

  • DbLoaderConfig:你希望接受的配置的 TypeScript 类型。
  • DbLoader:作为类的加载器实现。它必须遵循 TranslationsLoaderContract 接口。
  • dbLoader:最后,一个工厂函数,用于在配置文件中引用加载器。

使用加载器

创建加载器后,你可以使用 dbLoader 工厂函数在配置文件中引用它。

import { defineConfig } from '@adonisjs/i18n'
import { dbLoader } from 'my-custom-package'
const i18nConfig = defineConfig({
loaders: [
dbLoader({
connection: 'pg',
tableName: 'translations'
})
]
})

创建自定义翻译格式化器

翻译格式化器负责根据特定格式格式化翻译。我们提供了一个 ICU 消息语法的实现,并提供了额外的 API 来注册自定义格式化器。

格式化器必须实现 TranslationsFormatterContract 接口,并定义 format 方法来格式化翻译消息。

import type {
FormatterFactory,
TranslationsLoaderContract,
} from '@adonisjs/i18n/types'
/**
* 格式化器实现
*/
export class FluentFormatter implements TranslationsFormatterContract {
format(
message: string,
locale: string,
data?: Record<string, any>
): string {
// 返回格式化后的值
}
}
/**
* 工厂函数,用于在配置文件中引用格式化器。
*/
export function fluentFormatter(): FormatterFactory {
return () => {
return new FluentFormatter()
}
}

使用格式化器

创建格式化器后,你可以使用 fluentFormatter 工厂函数在配置文件中引用它。

import { defineConfig } from '@adonisjs/i18n'
import { fluentFormatter } from 'my-custom-package'
const i18nConfig = defineConfig({
formatter: fluentFormatter()
})