国际化与本地化

本指南介绍 AdonisJS 应用中的国际化(i18n)与本地化。你将学习如何:

  • 配置 i18n 包并设置受支持的语言区域
  • 为多种语言存储和组织翻译文件
  • 在控制器与 Edge 模板中解析和格式化翻译
  • 自动翻译验证错误消息
  • 使用 ICU 消息格式处理动态内容(复数、日期、数字、性别)
  • 格式化货币、日期、相对时间等值
  • 创建自定义的翻译加载器与格式化器

概述

在为全球受众构建应用时,你需要两种能力:本地化(将文本翻译成多种语言)与国际化(根据地区习惯格式化日期、数字、货币等值)。@adonisjs/i18n 包同时提供了这两者。

本地化需要为应用支持的每种语言编写翻译,并在 Edge 模板、验证消息中引用它们,或直接通过 i18n API 引用。与其在代码库中到处硬编码 "Welcome back!" 这样的字符串,不如将翻译存储在专门的文件中,并按键名查找。这样无需修改应用代码即可轻松添加新语言。

国际化负责格式化层面的工作。同一个日期,在美国可能显示为 "January 10, 2026",而在法国可能显示为 "10 janvier 2026"。i18n 包底层使用浏览器标准的 Intl API,为数字、货币、日期、时间等提供区域感知的格式化。

该包通过中间件与 AdonisJS 集成:中间件从 Accept-Language 请求头中检测用户的偏好语言,创建一个特定于该语言区域的 i18n 实例,并通过 HTTP 上下文在整个请求生命周期中可用。

安装

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

node ace add @adonisjs/i18n
查看 add 命令执行的步骤
  1. 使用检测到的包管理器安装 @adonisjs/i18n 包。

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

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

  4. middleware 目录中创建 detect_user_locale_middleware

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

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

配置

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

config/i18n.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

另见: 配置桩文件

配置选项

选项说明
defaultLocale当应用不支持用户语言时使用的兜底语言区域。翻译与值格式化都会回退到该语言区域。
formatter存储翻译时使用的消息格式。AdonisJS 默认使用 ICU 消息格式 ,这是 Crowdin、Lokalise 等翻译服务广泛支持的通用标准。你也可以 创建自定义格式化器
fallbackLocales定义语言区域之间兜底关系的键值对。例如,你可能希望向使用加泰罗尼亚语的用户展示西班牙语内容。
supportedLocales应用支持的语言区域数组。如果省略,将根据翻译文件推断。
loaders用于加载翻译的加载器集合。默认的文件系统加载器从 resources/lang 读取。你可以 创建自定义加载器 ,从数据库或远程服务加载翻译。

配置兜底语言区域

当某个语言区域缺少翻译时,i18n 包会先回退到相关语言,再回退到默认语言区域。这对于区域性变体非常有用。

config/i18n.ts
export default defineConfig({
  formatter: formatters.icu(),
  defaultLocale: 'en',
  fallbackLocales: {
    'de-CH': 'de',  // 瑞士德语回退到德语
    'fr-CH': 'fr',  // 瑞士法语回退到法语
    ca: 'es'        // 加泰罗尼亚语回退到西班牙语
  }
})

配置受支持的语言区域

默认情况下,该包会根据翻译文件推断受支持的语言区域。如果你拥有 enfres 的翻译目录,它们会自动成为受支持的语言区域。如需显式定义受支持的语言区域,请使用 supportedLocales 选项。

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

存储翻译

翻译存储在 resources/lang 目录中。为每种语言创建一个子目录,使用 IETF 语言标签 格式(如 enfrde)。

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

对于区域性变体,使用地区代码创建子目录。当某个键缺失时,AdonisJS 会自动从区域性翻译回退到基础翻译。

resources
├── lang
   ├── en        # 英语(基础)
   ├── en-us     # 英语(美国)
   └── en-gb     # 英语(英国)

另见: ISO 语言代码

翻译文件格式

翻译存储在 .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 类实例。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

/**
 * 为特定语言区域创建 I18n 实例
 */
const en = i18nManager.locale('en')
const fr = i18nManager.locale('fr')

使用 .t 方法按键名格式化翻译。键名遵循 文件名.路径.到.键 的模式。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

const i18n = i18nManager.locale('en')
i18n.t('messages.greeting') // "Hello world"
app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

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

理解回退行为

每个 I18n 实例都基于你的 fallbackLocales 配置预置了一个兜底语言。当主语言缺少翻译时,会使用兜底语言。

config/i18n.ts
export default defineConfig({
  defaultLocale: 'en',
  fallbackLocales: {
    'de-CH': 'de',
    'fr-CH': 'fr'
  }
})
app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

const swissGerman = i18nManager.locale('de-CH')
swissGerman.fallbackLocale // "de"(来自 fallbackLocales)

const swissFrench = i18nManager.locale('fr-CH')
swissFrench.fallbackLocale // "fr"(来自 fallbackLocales)

const english = i18nManager.locale('en')
english.fallbackLocale // "en"(使用 defaultLocale)

处理缺失的翻译

当主语言和兜底语言都缺少某条翻译时,.t 方法会返回一个错误字符串。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

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

要将其替换为自定义的兜底值,请将该值作为第二个参数传入。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

const i18n = i18nManager.locale('en')
i18n.t('messages.hero_title', '')
// ""(空字符串,而非错误消息)

如需全局兜底策略,请在配置中定义 fallback 函数。该函数接收翻译路径与语言区域代码,且必须返回一个字符串。

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

export default defineConfig({
  defaultLocale: 'en',
  formatter: formatters.icu(),
  fallback: (identifier, locale) => {
    return '' // 为所有缺失的翻译返回空字符串
  },
})

在 HTTP 请求中检测用户语言区域

安装时创建的 detect_user_locale_middleware 会自动处理语言区域检测。它从传入请求中读取 Accept-Language 请求头 ,为检测到的语言区域创建 I18n 实例,并通过 HTTP 上下文 共享该实例。

启用该中间件后,你就可以在控制器和 Edge 模板中访问翻译。

app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async store({ i18n, session, response }: HttpContext) {
    // Create post...

    session.flash('success', {
      message: i18n.t('post.created')
    })

    return response.redirect().back()
  }
}

在 Edge 模板中,使用 t 辅助函数。

resources/views/welcome.edge
<h1>{{ t('messages.heroTitle') }}</h1>

自定义语言区域检测

该中间件是你应用代码库的一部分,因此你可以修改 getRequestLocale 方法以使用自定义的探测逻辑。例如,你可能会从用户档案、Cookie 或 URL 参数中读取语言区域,而非使用 Accept-Language 请求头。

翻译验证消息

detect_user_locale_middleware 会钩入 请求验证器 ,从你的翻译文件中提供验证消息。

app/middleware/detect_user_locale_middleware.ts
export default class DetectUserLocaleMiddleware {
  static {
    RequestValidator.messagesProvider = (ctx) => {
      return ctx.i18n.createMessagesProvider()
    }
  }
}

将验证翻译存储在 validator.json 文件中的 shared 键下。可以为验证规则或特定的 字段 + 规则 组合定义消息。

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 请求期间,中间件会自动为验证注册一个 自定义消息提供者 。当在 HTTP 请求之外(如在 Ace 命令或队列任务中)使用 VineJS 时,需要显式注册消息提供者。

app/jobs/create_user_job.ts
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(International Components for Unicode,Unicode 国际组件)消息格式是存储需要动态内容的翻译的标准。它支持插值、数字格式化、日期格式化、复数化和基于性别的选择。

插值

使用单花括号引用动态值。

resources/lang/en/messages.json
{
  "greeting": "Hello { username }"
}
resources/views/welcome.edge
{{ t('messages.greeting', { username: 'Virk' }) }}
{{-- 输出:Hello Virk --}}
Tip

ICU 消息语法 不支持嵌套数据集 。在插值过程中你只能访问扁平对象的属性。如果需要嵌套数据,请在传入翻译函数之前将其扁平化。

要在翻译中包含 HTML,请在 Edge 模板中使用三个花括号以防止转义。

resources/lang/en/messages.json
{
  "greeting": "<p>Hello <strong>{ username }</strong></p>"
}
resources/views/welcome.edge
{{{ t('messages.greeting', { username: 'Virk' }) }}}

数字格式化

使用 {键, number, 格式} 语法格式化数值。格式可以包含 数字骨架 以进行精确控制。

resources/lang/en/messages.json
{
  "bagel_price": "The price of this bagel is {amount, number, ::currency/USD}"
}
resources/views/menu.edge
{{ t('bagel_price', { amount: 2.49 }) }}
{{-- 输出:The price of this bagel is $2.49 --}}

更多格式化示例:

消息输出
Length: {length, number, ::measure-unit/length-meter}Length: 5 m
Balance: {price, number, ::currency/USD compact-long}Balance: $1 thousand

日期与时间格式化

使用 {键, date, 格式}{键, time, 格式} 语法格式化 Date 对象或 Luxon DateTime 实例。

resources/lang/en/messages.json
{
  "shipment_update": "Your package will arrive on {expectedDate, date, medium}"
}
resources/views/order.edge
{{ t('shipment_update', { expectedDate: luxonDateTime }) }}
{{-- 输出:Your package will arrive on Oct 16, 2023 --}}

对于时间格式化:

resources/lang/en/messages.json
{
  "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 对复数化提供了一流的支持。plural 格式根据数值选择不同的文本。

resources/lang/en/messages.yaml
cart_summary: >
  You have {itemsCount, plural,
    =0 {no items}
    one {1 item}
    other {# items}
  } in your cart
resources/views/cart.edge
{{ t('messages.cart_summary', { itemsCount: 1 }) }}
{{-- 输出:You have 1 item in your cart --}}

# 符号是数值的占位符,会根据语言区域规则进行格式化。

resources/views/cart.edge
{{ t('messages.cart_summary', { itemsCount: 1000 }) }}
{{-- 输出:You have 1,000 items in your cart --}}

复数类别

{键, plural, 匹配项} 语法会将值与以下类别进行匹配:

类别使用场景
zero拥有「零个」语法的语言(阿拉伯语、拉脱维亚语)
one拥有「一个」语法的语言(许多西方语言)
two拥有「两个」语法的语言(阿拉伯语、威尔士语)
few拥有「少量」语法的语言(因语言而异)
many拥有「大量」语法的语言(阿拉伯语、波兰语、俄语)
other当没有其他类别匹配时的默认类别;在英语等语言中用作「复数」
=value精确匹配一个特定的数值

Select 格式

select 格式通过将某个值与多个选项进行匹配来选择输出。这对于性别相关的文本或其他分类选择非常有用。

resources/lang/en/messages.yaml
auto_reply: >
  {gender, select,
    male {He}
    female {She}
    other {They}
  } will respond shortly.
resources/views/contact.edge
{{ t('messages.auto_reply', { gender: 'female' }) }}
{{-- 输出:She will respond shortly. --}}

Selectordinal 格式

selectordinal 格式根据语言区域规则处理序数(第 1、第 2、第 3 等)。

resources/lang/en/messages.yaml
anniversary_greeting: >
  It's my {years, selectordinal,
    one {#st}
    two {#nd}
    few {#rd}
    other {#th}
  } anniversary
resources/views/profile.edge
{{ t('messages.anniversary_greeting', { years: 2 }) }}
{{-- 输出:It's my 2nd anniversary --}}

序数类别与复数类别相同(zeroonetwofewmanyother=value)。

格式化值

i18n 包提供了根据语言区域习惯格式化值的方法。这些方法使用了性能经过优化的 Node.js Intl API

formatNumber

使用 Intl.NumberFormat 类格式化数值。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatNumber(123456.789, {
    maximumSignificantDigits: 3
  })
// "123,000"

参见: Intl.NumberFormat 选项

formatCurrency

将数值格式化为货币。该方法会自动设置 style: 'currency'

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatCurrency(200, {
    currency: 'USD'
  })
// "$200.00"

formatDate

使用 Intl.DateTimeFormat 类格式化 Date 对象或 Luxon DateTime 实例。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'
import { DateTime } from 'luxon'

i18nManager
  .locale('en')
  .formatDate(new Date(), {
    dateStyle: 'long'
  })
// "January 10, 2026"

i18nManager
  .locale('en')
  .formatDate(DateTime.local(), {
    dateStyle: 'long'
  })
// "January 10, 2026"

参见: Intl.DateTimeFormat 选项

formatTime

将日期值格式化为时间字符串。该方法默认设置 timeStyle: 'medium'

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatTime(new Date())
// "2:48:30 PM"

formatRelativeTime

使用 Intl.RelativeTimeFormat 类将值格式化为相对时间(如 "in 2 hours" 或 "3 days ago")。

app/services/example_service.ts
import { DateTime } from 'luxon'
import i18nManager from '@adonisjs/i18n/services/main'

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

i18nManager
  .locale('en')
  .formatRelativeTime(futureDate, 'hours')
// "in 2 hours"

使用 'auto' 单位可自动选择最合适的单位。

app/services/example_service.ts
import { DateTime } from 'luxon'
import i18nManager from '@adonisjs/i18n/services/main'

const nearFuture = DateTime.local().plus({ hours: 2 })
i18nManager.locale('en').formatRelativeTime(nearFuture, 'auto')
// "in 2 hours"

const farFuture = DateTime.local().plus({ hours: 200 })
i18nManager.locale('en').formatRelativeTime(farFuture, 'auto')
// "in 8 days"

参见: Intl.RelativeTimeFormat 选项

formatPlural

使用 Intl.PluralRules 类查找一个数字对应的复数类别。当你需要在代码中(而非翻译字符串中)处理复数化时,这会很有用。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager.locale('en').formatPlural(0)  // "other"
i18nManager.locale('en').formatPlural(1)  // "one"
i18nManager.locale('en').formatPlural(2)  // "other"

参见: Intl.PluralRules 选项

formatList

使用 Intl.ListFormat 类将字符串数组格式化为可读的列表。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

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

i18nManager
  .locale('en')
  .formatList(['5 hours', '3 minutes'], { type: 'unit' })
// "5 hours, 3 minutes"

参见: Intl.ListFormat 选项

formatDisplayNames

使用 Intl.DisplayNames 类将货币、语言、地区、日历的代码格式化为可读的名称。

app/services/example_service.ts
import i18nManager from '@adonisjs/i18n/services/main'

i18nManager
  .locale('en')
  .formatDisplayNames('INR', { type: 'currency' })
// "Indian Rupee"

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

参见: Intl.DisplayNames 选项

配置 i18n Ally VSCode 扩展

VSCode 的 i18n Ally 扩展提供内联翻译预览、缺失翻译检测与快速编辑功能。要为 AdonisJS 配置它,请在 .vscode 目录中创建两个文件。

mkdir -p .vscode
touch .vscode/i18n-ally-custom-framework.yml
touch .vscode/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
languageIds:
  - edge
usageMatchRegex:
  - "[^\\w\\d]t\\(['\"`]({key})['\"`]"
sortKeys: true

监听缺失的翻译

订阅 i18n:missing:translation 事件以跟踪应用中的缺失翻译。这对于开发期间的日志记录或告警很有用。

start/events.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)       // 请求的语言区域
})

重新加载翻译

i18n 包在启动时加载翻译文件并将其缓存在内存中。如果在应用运行期间修改了翻译文件,请使用 reloadTranslations 方法来刷新缓存。

app/controllers/admin_controller.ts
import i18nManager from '@adonisjs/i18n/services/main'

export default class AdminController {
  async refreshTranslations() {
    await i18nManager.reloadTranslations()
  }
}

进阶:创建自定义翻译加载器

翻译加载器负责从持久化存储中加载翻译。默认的文件系统加载器从 resources/lang 读取,但你可以创建自定义加载器,从数据库、远程 API 或任何其他来源读取。

加载器必须实现 TranslationsLoaderContract 接口,并提供一个 load 方法,该方法返回按语言区域分组的翻译。

app/i18n/db_loader.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',
        'messages.farewell': 'Goodbye',
      },
      fr: {
        'messages.greeting': 'Bonjour le monde',
        'messages.farewell': 'Au revoir',
      }
    }
  }
}

/**
 * 在配置中引用加载器的工厂函数
 */
export function dbLoader(config: DbLoaderConfig): LoaderFactory {
  return () => {
    return new DbLoader(config)
  }
}

使用自定义加载器

使用工厂函数在配置文件中引用该加载器。

config/i18n.ts
import { defineConfig } from '@adonisjs/i18n'
import { dbLoader } from '#app/i18n/db_loader'

const i18nConfig = defineConfig({
  defaultLocale: 'en',
  formatter: formatters.icu(),
  loaders: [
    dbLoader({
      connection: 'pg',
      tableName: 'translations'
    })
  ]
})

export default i18nConfig

进阶:创建自定义翻译格式化器

翻译格式化器会根据特定格式处理翻译字符串。默认的 ICU 格式化器处理 ICU 消息语法,但你可以为其他格式(如 Fluent)创建自定义格式化器。

格式化器必须实现 TranslationsFormatterContract 接口,并提供一个 format 方法。

app/i18n/fluent_formatter.ts
import type {
  FormatterFactory,
  TranslationsFormatterContract,
} from '@adonisjs/i18n/types'

/**
 * 格式化器的实现
 */
export class FluentFormatter implements TranslationsFormatterContract {
  format(
    message: string,
    locale: string,
    data?: Record<string, any>
  ): string {
    /**
     * 使用你选择的格式处理消息,
     * 并返回格式化后的字符串
     */
    return message // 在此编写你的格式化逻辑
  }
}

/**
 * 在配置中引用格式化器的工厂函数
 */
export function fluentFormatter(): FormatterFactory {
  return () => {
    return new FluentFormatter()
  }
}

使用自定义格式化器

使用工厂函数在配置文件中引用该格式化器。

config/i18n.ts
import { defineConfig } from '@adonisjs/i18n'
import { fluentFormatter } from '#app/i18n/fluent_formatter'

const i18nConfig = defineConfig({
  defaultLocale: 'en',
  formatter: fluentFormatter()
})

export default i18nConfig