国际化(i18n)多语言支持
国际化不只是"把文字翻译成另一种语言"。它涉及路由策略、SEO 优化、语言包加载性能、日期/货币/数字的本地化格式、RTL 布局适配等一系列工程决策。
@nuxtjs/i18n是 Nuxt 生态中最成熟的 i18n 方案,本章从路由策略的底层取舍讲起,覆盖 SEO、性能优化到中英双语的完整落地。
1. 路由策略:i18n 的第一个关键决策
1.1 四种路由策略
@nuxtjs/i18n 提供四种路由策略,每种都有不同的 URL 表现:
| 策略 | 默认语言 URL | 其他语言 URL | 适用场景 |
|---|---|---|---|
prefix_except_default | /about | /en/about | 最常见,默认语言无前缀 |
prefix | /zh/about | /en/about | 所有语言等价对待 |
prefix_and_default | /zh/about 和 /about | /en/about | 兼容旧 URL |
no_prefix | /about | /about | 单 URL,Cookie/Header 切换 |
1.2 如何选择
prefix_except_default(推荐):最符合用户习惯。中文用户访问 /about 看到中文,英文用户访问 /en/about 看到英文。默认语言没有前缀,URL 更简洁。
prefix:所有语言都有前缀。优势是 URL 结构完全一致,劣势是默认语言也需要前缀(/zh/about),已有的不带前缀的链接会 404。
no_prefix:URL 不变,通过 Cookie 或 Accept-Language Header 判断语言。优势是 URL 简洁,劣势是 SEO 极差——搜索引擎无法区分同一 URL 的不同语言版本。除非你的应用不需要 SEO(如后台管理系统),否则不要用。
1.3 配置
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'zh', language: 'zh-CN', name: '中文', file: 'zh.json' },
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
],
defaultLocale: 'zh',
strategy: 'prefix_except_default',
lazy: true,
langDir: 'locales/',
},
})2. 翻译管理
2.1 语言文件组织
小项目可以用单个 JSON 文件:
locales/
├── zh.json
└── en.json
中大型项目按模块拆分:
locales/
├── zh/
│ ├── common.json ← 全局通用(导航、按钮、错误消息)
│ ├── video.json ← 视频模块
│ ├── auth.json ← 认证模块
│ └── admin.json ← 后台模块
├── en/
│ ├── common.json
│ ├── video.json
│ ├── auth.json
│ └── admin.json
2.2 翻译键的命名规范
好的键命名让翻译文件可维护:
{
"video": {
"title": "视频标题",
"upload": {
"button": "上传视频",
"progress": "上传中... {percent}%",
"success": "上传成功",
"error": "上传失败,请重试"
},
"player": {
"play": "播放",
"pause": "暂停",
"quality": {
"auto": "自动",
"1080p": "1080p 高清",
"720p": "720p 标清"
}
}
}
}命名原则:
- 模块.功能.具体项:层级不超过 3-4 级
- 名词优先:
video.upload.button而非video.clickToUpload - 带参数的翻译:用
{param}占位符,如"共 {count} 个视频"
2.3 在组件中使用
<template>
<div>
<h1>{{ $t('video.title') }}</h1>
<p>{{ $t('video.upload.progress', { percent: 75 }) }}</p>
<button>{{ $t('video.upload.button') }}</button>
</div>
</template>
<script setup>
const { t, locale, setLocale } = useI18n()
// 编程式访问
console.log(t('video.title'))
// 切换语言
await setLocale('en')
</script>$t() 是模板中的翻译函数,useI18n() 的 t() 是 Composition API 版本。
3. SEO 优化
3.1 hreflang 标签
hreflang 告诉搜索引擎"这个页面还有其他语言版本",是多语言 SEO 的核心:
<!-- /about 页面的 <head> -->
<link rel="alternate" hreflang="zh-CN" href="https://example.com/about" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />@nuxtjs/i18n 自动生成 hreflang 标签,无需手动配置。x-default 指向默认语言版本,供搜索引擎在没有匹配语言时使用。
3.2 Sitemap 多语言
配合 @nuxtjs/sitemap,可以为每个页面生成多语言 sitemap:
<url>
<loc>https://example.com/about</loc>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://example.com/about"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/en/about"/>
</url>3.3 SEO 元数据的翻译
页面的 title 和 description 也需要翻译:
<script setup>
const { t } = useI18n()
useHead({
title: t('video.seo.title'),
meta: [
{ name: 'description', content: t('video.seo.description') },
],
})
</script>3.4 canonical URL
多语言页面的 canonical URL 应该指向当前语言版本的 URL,而非默认语言。@nuxtjs/i18n 正确处理了这一点——/en/about 的 canonical 是 https://example.com/en/about,而非 https://example.com/about。
4. 性能优化:懒加载语言包
4.1 问题
如果所有语言的翻译文件都打包在初始 bundle 中,每增加一种语言,bundle 就增大一份。10 种语言 × 每种 50KB = 500KB 的翻译文件——用户只看一种语言,却下载了所有语言。
4.2 懒加载方案
lazy: true 开启后,只有当前语言的翻译文件会被加载。切换语言时动态加载目标语言的文件:
i18n: {
lazy: true, // 开启懒加载
langDir: 'locales/', // 语言文件目录
locales: [
{ code: 'zh', file: 'zh.json' },
{ code: 'en', file: 'en.json' },
],
}加载时序:
- 用户访问
/about(默认中文)→ 只加载zh.json - 用户点击"English" → 动态
import('locales/en.json')→ 切换完成
4.3 模块化语言文件的懒加载
如果语言文件按模块拆分,可以用目录格式:
locales: [
{
code: 'zh',
files: ['zh/common.json', 'zh/video.json', 'zh/auth.json'],
},
],或者进一步优化——只加载当前页面需要的语言包。这需要在页面组件中按需加载:
<script setup>
const { mergeLocaleMessage } = useI18n()
// 只在视频页面加载视频相关翻译
const videoMessages = await import(`~/locales/${locale.value}/video.json`)
mergeLocaleMessage(locale.value, { video: videoMessages.default })
</script>4.4 SSR 与语言包预加载
SSR 阶段,服务端需要正确的语言包来渲染页面。@nuxtjs/i18n 会在 SSR 时同步加载当前语言的翻译文件,确保首屏 HTML 包含正确的翻译内容。客户端 Hydration 时复用服务端的翻译数据,不会重复请求。
5. 本地化格式
5.1 日期和时间
不同地区的日期格式差异巨大:
| 地区 | 格式 | 示例 |
|---|---|---|
| 中国 | YYYY年MM月DD日 | 2025年1月15日 |
| 美国 | MM/DD/YYYY | 01/15/2025 |
| 英国 | DD/MM/YYYY | 15/01/2025 |
| 日本 | YYYY/MM/DD | 2025/01/15 |
使用 Intl.DateTimeFormat(浏览器原生 API)或 Vue I18n 的日期格式化:
// i18n 配置中定义日期格式
datetimeFormats: {
zh: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
},
en: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
},
}<template>
<time>{{ $d(new Date(), 'long') }}</time>
<!-- 中文: 2025年1月15日 14:30 -->
<!-- 英文: January 15, 2025, 2:30 PM -->
</template>5.2 数字和货币
numberFormats: {
zh: {
currency: { style: 'currency', currency: 'CNY' },
decimal: { style: 'decimal', minimumFractionDigits: 2 },
},
en: {
currency: { style: 'currency', currency: 'USD' },
decimal: { style: 'decimal', minimumFractionDigits: 2 },
},
}<template>
<span>{{ $n(9999.5, 'currency') }}</span>
<!-- 中文: ¥9,999.50 -->
<!-- 英文: $9,999.50 -->
</template>5.3 复数形式
不同语言的复数规则差异很大。英文有单复数(1 item / 2 items),中文没有复数,阿拉伯语有 6 种复数形式。Vue I18n 用管道符 | 处理:
{
"video": {
"count": "没有视频 | {count} 个视频 | {count} 个视频"
}
}{
"video": {
"count": "no videos | {count} video | {count} videos"
}
}<template>
{{ $t('video.count', { count: videoCount }, videoCount) }}
</template>6. 语言切换
6.1 语言切换组件
<template>
<select :value="locale" @change="setLocale($event.target.value)">
<option v-for="loc in locales" :key="loc.code" :value="loc.code">
{{ loc.name }}
</option>
</select>
</template>
<script setup>
const { locale, locales, setLocale } = useI18n()
</script>6.2 语言检测
@nuxtjs/i18n 支持自动检测用户语言:
i18n: {
detectBrowserLanguage: {
useCookie: true, // 用 Cookie 记住选择
cookieKey: 'i18n_lang',
fallbackLocale: 'zh',
redirectOn: 'root', // 只在首页重定向
},
}检测优先级:Cookie(用户选择)→ Navigator Language(浏览器语言)→ defaultLocale(兜底)。
redirectOn: 'root' 很重要:只在访问根路径 / 时自动重定向到检测到的语言。如果设为 'all',每个页面都会检测并重定向,这会破坏分享链接——你分享 /en/about 给中文用户,他会被重定向到 /about。
6.3 客户端持久化
用户手动切换语言后,通过 Cookie 持久化选择。下次访问时优先使用 Cookie 中的语言,而非浏览器语言检测。
7. 翻译工作流
7.1 开发者 vs 翻译者
最佳实践是开发者写默认语言(中文),翻译者处理其他语言:
- 开发者在代码中使用
$t('video.upload.button') - 开发者在
zh.json中添加中文翻译 - 翻译者在
en.json中添加英文翻译 - CI 检查翻译完整性(所有键都有对应翻译)
7.2 翻译完整性检查
可以在 CI 中检查所有语言文件的键是否一致:
// scripts/check-i18n.ts
import zh from '../locales/zh.json'
import en from '../locales/en.json'
function getKeys(obj: object, prefix = ''): string[] {
return Object.entries(obj).flatMap(([key, value]) =>
typeof value === 'object'
? getKeys(value, `${prefix}${key}.`)
: [`${prefix}${key}`]
)
}
const zhKeys = new Set(getKeys(zh))
const enKeys = new Set(getKeys(en))
const missing = [...zhKeys].filter(k => !enKeys.has(k))
if (missing.length > 0) {
console.error('Missing English translations:', missing)
process.exit(1)
}7.3 翻译平台集成
大型项目可以用专业翻译平台管理翻译:
| 平台 | 特点 | 适用场景 |
|---|---|---|
| Crowdin | 开源项目免费,社区翻译 | 开源项目 |
| Lokalise | 开发者友好,CLI 同步 | 商业项目 |
| Phrase | 企业级,工作流审批 | 大型团队 |
| Tolgee | 开源自托管,页内翻译编辑 | 注重隐私 |
这些平台可以监听 Git 仓库中 locales/ 目录的变化,自动同步翻译键到平台,翻译完成后自动提交 PR。
本章小结
- 路由策略:
prefix_except_default最常见,no_prefixSEO 极差只适合后台系统 - 翻译管理:按模块拆分语言文件,键名遵循
模块.功能.具体项三级结构 - SEO:hreflang 自动生成,Sitemap 多语言支持,页面元数据也要翻译
- 懒加载:
lazy: true只加载当前语言,切换时动态加载,SSR 阶段同步加载保证首屏 - 本地化格式:日期
$d()、数字$n()、复数用管道符,依赖IntlAPI - 语言检测:Cookie 优先 → 浏览器语言 → 默认值,
redirectOn: 'root'避免破坏分享链接 - 翻译工作流:开发者写默认语言,翻译者处理其他语言,CI 检查翻译完整性