國際化 (i18n)

基於 @nuxtjs/i18n 建構的多語言支援,內建簡體中文、繁體中文、英文三語言,翻譯檔案扁平化管理,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,雙寫會互相覆蓋。