国际化 (i18n)

基于 @nuxtjs/i18n 构建的多语言支持——framework 层默认仅 zh-CN,多语言经 cli 创建时多选或显式传 locales 扩展(starter 模板内置简体中文、繁体中文、英文三份 locale 文件);翻译文件扁平化管理,AI 友好。

启用 i18n

// nuxt.config.ts
swiftConfig: {
  i18n: {
    enabled: true,
    defaultLocale: 'zh-CN',
    locales: ['zh-CN', 'zh-TW', 'en'],
  }
}

swiftConfig.i18n.locales 只做一件事:把语言码映射成 @nuxtjs/i18n 的 { code, language } 用于生成路由前缀(/en/、/zh-TW/)。它不注册翻译文件 —— 里面没有 file 字段。

翻译文件靠 nuxt.config.ts 顶层的 i18n 块接上(sw config set framework.i18n.enabled true 会自动解开 starter 里这段注释):

// nuxt.config.ts —— 与 swiftConfig 平级的顶层 i18n
i18n: {
  langDir: 'locales',                                    // 相对 i18n/ 目录
  locales: [
    { code: 'zh-CN', language: 'zh-CN', name: '简体中文', file: 'zh-CN.json' },
    { code: 'zh-TW', language: 'zh-TW', name: '繁體中文', file: 'zh-TW.json' },
    { code: 'en',    language: 'en',    name: 'English',  file: 'en.json' },
  ],
}

顶层 i18n.locales 会覆盖 framework 给的默认值,所以两处的语言码要保持一致。只配 swiftConfig.i18n.locales、不配顶层 file 的后果是:路由前缀正常、$t() 却拿不到任何翻译(原样吐 key)。策略 / 浏览器检测 / bundle 等其余项由 framework 提供默认值,不必重复写。

framework 内置文案的多语言行为

framework 组件自身也有文案(分页器的「共 N 条」、feed 的「N 秒后可关闭广告」、弹窗的 「关闭 / 取消 / 确认」,以及约 33 条 aria-label)。这些会自动跟随你的活动语言,业务零改动。

  • 启用 i18n 时:跟随用户当前选择的语言(不是构建期的 defaultLocale)—— 用户点 SwLangSwitch 切换,framework 文案当场跟着变
  • 未启用 i18n 时:一律中文,零额外开销
  • 内置 zh-CN / zh-TW / en 三语;选了其它语言(如 ja)时回落中文,不会出现空白
  • 语言码按主码归一:zh-Hant / zh-HK / zh-MO 都归繁体,en-US / en-GB 都归英文
<!-- 你什么都不用做:切到 en 后分页器自己就是英文 -->
<SwPagination v-model:page="page" :total="total" show-total />
<!-- zh-CN → 「共 100 条」/ zh-TW → 「共 100 筆」/ en → 「100 items」 -->

已有 slot / prop 逃生舱的组件不受影响——SwModal 的 cancelText / confirmText、 SwGallery 的 #empty / #finished 等你传了就用你的,没传才走内置词条。

翻译文件结构

翻译文件位于 i18n/locales/ 目录,采用扁平化 JSON 格式:

// i18n/locales/zh-CN.json
{
  "首页": "首页",
  "快速上手": "快速上手",
  "立即开始": "立即开始",
  "首页SEO描述": "Swift Web 框架的官方文档站,涵盖组件库和开发规范"
}
// i18n/locales/en.json
{
  "首页": "Home",
  "快速上手": "Quick Start",
  "立即开始": "Get Started",
  "首页SEO描述": "Official docs for Swift Web framework, covering components and standards"
}

在页面中使用

<script setup lang="ts">
const { t } = useI18n()

useSeo({
  title: () => t('首页'),
  description: () => t('首页SEO描述'),
})
</script>

<template>
  <h1>{{ t('快速上手') }}</h1>
  <button>{{ t('立即开始') }}</button>
</template>

语言切换

<template>
  <!-- 内置切换器:仅 i18n 启用且 locales ≥ 2 时渲染(自 gate,否则占 0 空间);
       支持 token(--sw-lang-switch-*)/ 具名 icon slot / default 作用域 slot 三档定制 -->
  <SwLangSwitch />
</template>

需要完全自定义 UI 时再手写(注意用 <option :selected> 而非 <select :value>——后者是非法 HTML,SSR 直出会选错项):

<script setup lang="ts">
const { locale, setLocale, locales } = useI18n()
</script>

<template>
  <select @change="setLocale(($event.target as HTMLSelectElement).value)">
    <option v-for="loc in locales" :key="loc.code" :value="loc.code" :selected="loc.code === locale">
      {{ loc.name }}
    </option>
  </select>
</template>

翻译 Key 规范

  • 短文案(≤50字):直接用中文原文作为 key,如 t('立即开始')
  • 长文案(>50字):用简短中文描述作 key,全文放在 JSON value 里,如 t('首页SEO描述')
  • 禁止将长句直接作为 key,否则翻译文件难以阅读

URL 本地化

@nuxtjs/i18n 默认使用路径前缀策略:

  • zh-CN(默认语言):example.com/(无前缀)
  • zh-TW:example.com/zh-TW/
  • en:example.com/en/

canonical 标签由 useSeo() 统一注入;多语言站(locales ≥ 2、prefix 策略)的 sitemap 自动按 locale 拆分并输出 hreflang 互链(零配置,详见 SEO)。

浏览器语言自动检测在可缓存页上不生效

@nuxtjs/i18n 的 detectBrowserLanguage(按 Accept-Language / Cookie 把访客带到对应语言前缀)在 ISR / 可缓存页上结构性失效:nitro 的缓存包装只向渲染透传 cache.varies 白名单里的请求头(host / x-forwarded-proto),Accept-Language 与 Cookie 根本到不了检测逻辑。这不是配置问题,把它们加进 varies 也不是出路——varies 会进缓存 key,按 Cookie 分裂等于命中率归零。

实际行为与取舍:

  • 直达可缓存页(含首页)不会发生语言自动跳转,所有访客拿到默认语言的缓存副本。这是确定性行为——也因此检测重定向不可能被缓存广播,无正确性风险。
  • 访客换语言靠 SwLangSwitch,路径前缀(/en/...)是语言的持久载体。
  • framework 会把可缓存页渲染期写出的 Set-Cookie(含 i18n 的检测标记 cookie)从响应剥离:可缓存响应带 Set-Cookie 会进共享缓存副本回放,还会让未配 proxy_ignore_headers 的 nginx / CDN 拒绝缓存整页(排障判据见部署手册)。dev 模式下剥离伴随 console 告警说明剥了哪个 cookie;需要按用户写 Cookie 请在客户端写,或把该路由改为不可缓存。

html lang 自动跟随语言

多语言站(locales ≥ 2)下,framework 通过内置运行时插件把 <html lang> 接管为当前语言(取 locale 的 language 字段,兜底 code):

  • 访问 /en/xxx → <html lang="en">,SSR 首帧即正确
  • 站内切语言(SwLangSwitch)→ lang 实时跟随
  • 单语言站 / i18n 关闭时保持构建期静态值(i18n.defaultLocale),零行为变化

applier 零配置,也不要在业务侧再用 useHead({ htmlAttrs: { lang } }) 双写 —— htmlAttrs 所有权在 framework,双写会互相覆盖。