Skip to content

国际化和本地化

国际化和本地化旨在帮助您为多个地区和语言创建 Web 应用程序。i18n(国际化的简称)支持由 @adonisjs/i18n 包提供。

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

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

安装

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

sh
node ace add @adonisjs/i18n

:::disclosure

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

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

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

  4. middleware 目录中创建 detect_user_locale_middleware

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

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

:::

配置

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

另请参阅:配置模板

ts
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

定义语言环境及其回退语言环境集合的键值对。例如,如果您的应用程序支持西班牙语,您可以将其定义为加泰罗尼亚语的回退。

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

supportedLocales

应用程序支持的语言环境数组。

ts
export default defineConfig({
  formatter: formatters.icu(),
  defaultLocale: 'en',
  // highlight-start
  supportedLocales: ['en', 'fr', 'it']
  // highlight-end
})

如果您不定义此值,我们将从翻译中推断 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 消息语法格式化。

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

解析翻译

在查找和格式化翻译之前,您必须使用 i18nManager.locale 方法创建一个特定于语言环境的 I18n 类实例。

ts
import i18nManager from '@adonisjs/i18n/services/main'

// 英语的 I18n 实例
const en = i18nManager.locale('en')

// 法语的 I18n 实例
const fr = i18nManager.locale('fr')

获得 I18n 类的实例后,您可以使用 .t 方法格式化翻译。

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

回退语言环境

每个实例都有一个基于 config.fallbackLocales 集合预配置的回退语言。当主语言缺少翻译时使用回退语言。

ts
export default defineConfig({
  fallbackLocales: {
    'de-CH': 'de',
    'fr-CH': 'fr'
  }
})
ts
const i18n = i18nManager.locale('de-CH')
i18n.fallbackLocale // de(使用回退集合)
ts
const i18n = i18nManager.locale('fr-CH')
i18n.fallbackLocale // fr(使用回退集合)
ts
const i18n = i18nManager.locale('en')
i18n.fallbackLocale // en(使用默认语言环境)

缺少的翻译

如果主语言环境和回退语言环境中都缺少翻译,.t 方法将返回格式如下的错误字符串。

ts
const i18n = i18nManager.locale('en')

i18n.t('messages.hero_title')
// translation missing: en, messages.hero_title

您可以通过将回退值定义为第二个参数来用不同的消息或空字符串替换此消息。

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

您还可以通过配置文件全局计算回退值。fallback 方法接收翻译路径作为第一个参数,语言环境代码作为第二个参数。确保始终返回字符串值。

ts
import { defineConfig } from '@adonisjs/i18n'

export default defineConfig({
  fallback: (identifier, locale) => {
    return ''
  },
})

在 HTTP 请求期间检测用户语言环境

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

  • 使用 Accept-language 标头检测请求的语言环境。

  • 为请求语言环境创建 I18n 类的实例,并使用 HTTP Context 与请求管道的其余部分共享。

  • 将相同的实例作为全局 i18n 属性与 Edge 模板共享。

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

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

ts
import { HttpContext } from '@adonisjs/core/http'

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

更改用户语言检测代码

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

翻译验证消息

detect_user_locale_middleware 挂钩到请求验证器,并使用翻译文件提供验证消息。

ts
export default class DetectUserLocaleMiddleware {
  static {
    // highlight-start
    RequestValidator.messagesProvider = (ctx) => {
      return ctx.i18n.createMessagesProvider()
    }
    // highlight-end
  }
}

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

json
// title: 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"
    }
  }
}
json
// title: 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"
    }
  }
}

ICU 消息格式

插值

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

TIP

ICU 消息语法不支持嵌套数据集,因此在插值期间您只能从扁平对象访问属性。

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

您还可以在消息中编写 HTML。但是,在 Edge 模板中使用三个花括号来渲染 HTML 而不转义它。

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

数字格式

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

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

日期/时间格式

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

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

复数规则

ICU 消息语法对在消息中定义复数规则有一流的支持。例如:

TIP

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

yaml
cart_summary:
  "You have {itemsCount, plural,
    =0 {no items}
    one {1 item}
    other {# items}
  } in your cart"
edge
{{ t('messages.cart_summary', { itemsCount: 1 }) }}
You have 1 item in your cart

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

edge
{{ t('messages.cart_summary', { itemsCount: 1000 }) }}

{{-- 输出 --}}
{{-- You have 1,000 items in your cart --}}

Select

select 格式允许您通过将值与多个选项之一匹配来选择输出。编写性别特定文本是 select 格式的一个很好的例子。

yaml
// title: Yaml
auto_reply:
  "{gender, select,
    male {He}
    female {She}
    other {They}
  } will respond shortly."
edge
{{ t('messages.auto_reply', { gender: 'female' }) }}
She will respond shortly.

格式化值

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

formatNumber

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

  1. 要格式化的值。
  2. 可选的 options 对象
ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatNumber(123456.789, {
    maximumSignificantDigits: 3
  })

formatCurrency

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

ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatCurrency(200, {
    currency: 'USD'
  })

formatDate

使用 Intl.DateTimeFormat 类格式化日期或 luxon date-time 对象。您可以传递以下参数。

  1. 要格式化的值。它可以是 Date 对象或 luxon DateTime 对象。
  2. 可选的 options 对象
ts
import i18nManager from '@adonisjs/i18n/services/main'
import { DateTime } from 'luxon'

i18nManager
  .locale('en')
  .formatDate(new Date(), {
    dateStyle: 'long'
  })

// 格式化 luxon date time 实例
i18nManager
  .locale('en')
  .formatDate(DateTime.local(), {
    dateStyle: 'long'
  })

formatTime

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

ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatTime(new Date())

formatRelativeTime

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

ts
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 以在最佳匹配单位中显示差异。

ts
const luxonDate = DateTime.local().plus({ hours: 2 })

I18n
  .locale('en')
  .formatRelativeTime(luxonDate, 'auto')

// In 2 hours 👈

formatPlural

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

  1. 要查找复数类别的数值。
  2. 可选的 options 对象。
ts
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 对象。
ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatList(['Me', 'myself', 'I'], { type: 'conjunction' })

// Me, myself and I

formatDisplayNames

使用 Intl.DisplayNames 类将 currencylanguageregioncalendar 代码格式化为其显示名称。您可以传递以下参数。

  1. 要格式化的代码。code 的值根据格式化的 type 而变化。
  2. Options 对象。
ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatDisplayNames('INR', { type: 'currency' })

// Indian Rupee
ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatDisplayNames('en-US', { type: 'language' })

// American English

配置 i18n Ally VSCode 扩展

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

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

sh
mkdir .vscode

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

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

json
// title: .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 文件中。

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

监听缺失翻译事件

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

ts
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 方法刷新内存缓存。

ts
import i18nManager from '@adonisjs/i18n/services/main'

await i18nManager.reloadTranslations()

创建自定义翻译加载器

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

加载器必须实现 TranslationsLoaderContract 接口并定义返回带有键值对的对象的 load 方法。键是语言环境代码,值是带有翻译列表的扁平对象。

ts
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)
  }
}

使用加载器

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

ts
import { defineConfig } from '@adonisjs/i18n'
import { dbLoader } from 'my-custom-package'

const i18nConfig = defineConfig({
  loaders: [
    dbLoader({
      connection: 'pg',
      tableName: 'translations'
    })
  ]
})

创建自定义翻译格式化器

翻译格式化器负责按照特定格式格式化翻译。我们附带 ICU 消息语法的实现并提供额外的 API 来注册自定义格式化器。

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

ts
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 工厂函数在配置文件中引用它。

ts
import { defineConfig } from '@adonisjs/i18n'
import { fluentFormatter } from 'my-custom-package'

const i18nConfig = defineConfig({
  formatter: fluentFormatter()
})