Nuxt 国际化(i18n)多语言方案完全指南
当你的应用需要面向不同语言的用户时,国际化(Internationalization,简称 i18n)就成为必须解决的问题。好的国际化方案不仅是简单的文字翻译,还涉及 URL 路由、SEO 优化、日期货币格式、RTL 布局等多个维度。
Nuxt 3 通过 @nuxtjs/i18n 模块提供了完整的国际化解决方案。本文将系统讲解如何在 Nuxt 应用中实现专业级的多语言支持。
快速开始
安装和基础配置
# 安装 i18n 模块
pnpm add @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'zh', iso: 'zh-CN', name: '中文', file: 'zh.json' },
{ code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
{ code: 'ja', iso: 'ja-JP', name: '日本語', file: 'ja.json' }
],
defaultLocale: 'zh',
langDir: 'locales/',
strategy: 'prefix_except_default',
lazy: true,
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_locale',
redirectOn: 'root'
}
}
})
翻译文件结构
locales/
├── zh.json # 中文翻译
├── en.json # 英文翻译
└── ja.json # 日语翻译
zh.json:
{
"common": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们",
"login": "登录",
"logout": "退出登录"
},
"home": {
"title": "欢迎来到 HTMLPAGE",
"description": "让网页构建更简单",
"cta": "立即开始"
},
"error": {
"notFound": "页面不存在",
"serverError": "服务器错误,请稍后重试"
}
}
en.json:
{
"common": {
"home": "Home",
"about": "About Us",
"contact": "Contact",
"login": "Login",
"logout": "Logout"
},
"home": {
"title": "Welcome to HTMLPAGE",
"description": "Make web building easier",
"cta": "Get Started"
},
"error": {
"notFound": "Page Not Found",
"serverError": "Server Error, please try again later"
}
}
在组件中使用
<script setup lang="ts">
const { t, locale, locales, setLocale } = useI18n()
</script>
<template>
<div>
<h1>{{ t('home.title') }}</h1>
<p>{{ t('home.description') }}</p>
<!-- 使用 $t 在模板中直接访问 -->
<button>{{ $t('home.cta') }}</button>
<!-- 语言切换 -->
<select :value="locale" @change="setLocale($event.target.value)">
<option v-for="loc in locales" :key="loc.code" :value="loc.code">
{{ loc.name }}
</option>
</select>
</div>
</template>
URL 路由策略
四种路由策略对比
| 策略 | 默认语言 URL | 其他语言 URL | 适用场景 |
|---|---|---|---|
no_prefix | /about | /about | 单语言或语言检测 |
prefix_except_default | /about | /en/about | 推荐:默认语言无前缀 |
prefix | /zh/about | /en/about | 所有语言平等对待 |
prefix_and_default | /about 或 /zh/about | /en/about | 兼容两种 URL |
推荐策略:prefix_except_default
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
strategy: 'prefix_except_default',
defaultLocale: 'zh',
// 生成的 URL 示例:
// 中文:/about, /products, /contact
// 英文:/en/about, /en/products, /en/contact
// 日语:/ja/about, /ja/products, /ja/contact
}
})
动态路由的语言切换
<script setup lang="ts">
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
// switchLocalePath 会保持当前路由参数
// 例如从 /products/123 切换到 /en/products/123
</script>
<template>
<nav>
<NuxtLink
v-for="loc in locales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
:class="{ active: locale === loc.code }"
>
{{ loc.name }}
</NuxtLink>
</nav>
</template>
本地化路由链接
<script setup lang="ts">
const localePath = useLocalePath()
</script>
<template>
<nav>
<!-- 自动根据当前语言生成正确的 URL -->
<NuxtLink :to="localePath('/')">{{ $t('common.home') }}</NuxtLink>
<NuxtLink :to="localePath('/about')">{{ $t('common.about') }}</NuxtLink>
<NuxtLink :to="localePath('/products')">{{ $t('common.products') }}</NuxtLink>
<!-- 带参数的路由 -->
<NuxtLink :to="localePath({ name: 'products-id', params: { id: '123' } })">
产品详情
</NuxtLink>
</nav>
</template>
翻译文件管理
嵌套结构组织
// locales/zh.json
{
"nav": {
"home": "首页",
"products": "产品",
"about": "关于"
},
"pages": {
"home": {
"hero": {
"title": "构建更好的网页",
"subtitle": "简单、快速、专业"
},
"features": {
"title": "核心功能",
"list": {
"speed": "极速加载",
"seo": "SEO 友好",
"responsive": "响应式设计"
}
}
},
"products": {
"title": "我们的产品",
"empty": "暂无产品"
}
},
"components": {
"footer": {
"copyright": "© 2024 HTMLPAGE. 保留所有权利。",
"links": {
"privacy": "隐私政策",
"terms": "服务条款"
}
}
}
}
使用嵌套键:
<template>
<h1>{{ $t('pages.home.hero.title') }}</h1>
<p>{{ $t('pages.home.features.list.speed') }}</p>
</template>
带参数的翻译
// locales/zh.json
{
"greeting": "你好,{name}!",
"items": "共 {count} 件商品",
"price": "价格:{price} 元"
}
<template>
<p>{{ $t('greeting', { name: userName }) }}</p>
<p>{{ $t('items', { count: itemCount }) }}</p>
<p>{{ $t('price', { price: formatPrice(amount) }) }}</p>
</template>
复数形式处理
// locales/en.json
{
"cart": {
"items": "no items | {count} item | {count} items"
},
"messages": {
"unread": "no messages | 1 message | {count} messages"
}
}
// locales/zh.json
{
"cart": {
"items": "购物车为空 | {count} 件商品"
}
}
<script setup>
const cartCount = ref(3)
</script>
<template>
<!-- 根据数量自动选择正确的复数形式 -->
<p>{{ $t('cart.items', cartCount) }}</p>
</template>
富文本翻译(HTML 支持)
{
"terms": {
"agreement": "我已阅读并同意 <a href=\"/terms\">服务条款</a> 和 <a href=\"/privacy\">隐私政策</a>"
}
}
<template>
<!-- 使用 v-html 渲染 HTML -->
<p v-html="$t('terms.agreement')"></p>
<!-- 更安全的方式:使用 i18n-t 组件 -->
<i18n-t keypath="terms.agreementSafe" tag="p">
<template #terms>
<NuxtLink to="/terms">{{ $t('terms.termsLink') }}</NuxtLink>
</template>
<template #privacy>
<NuxtLink to="/privacy">{{ $t('terms.privacyLink') }}</NuxtLink>
</template>
</i18n-t>
</template>
语言检测与切换
浏览器语言检测
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
detectBrowserLanguage: {
// 使用 Cookie 记住用户选择
useCookie: true,
cookieKey: 'i18n_locale',
cookieSecure: true,
// 重定向策略
redirectOn: 'root', // 仅在访问根路径时重定向
// 可选值:'root' | 'no prefix' | 'all'
// 语言检测来源
fallbackLocale: 'zh',
// Cookie 过期时间(天)
cookieExpires: 365
}
}
})
自定义语言切换组件
<!-- components/LanguageSwitcher.vue -->
<script setup lang="ts">
const { locale, locales, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
// 获取可用语言列表
const availableLocales = computed(() => {
return locales.value.filter(loc => loc.code !== locale.value)
})
// 获取当前语言信息
const currentLocale = computed(() => {
return locales.value.find(loc => loc.code === locale.value)
})
// 语言图标映射
const localeIcons: Record<string, string> = {
zh: '🇨🇳',
en: '🇺🇸',
ja: '🇯🇵',
ko: '🇰🇷'
}
</script>
<template>
<div class="language-switcher">
<button class="current-lang" @click="isOpen = !isOpen">
<span class="icon">{{ localeIcons[locale] }}</span>
<span class="name">{{ currentLocale?.name }}</span>
<ChevronDownIcon class="chevron" />
</button>
<div v-if="isOpen" class="dropdown">
<NuxtLink
v-for="loc in availableLocales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="lang-option"
@click="isOpen = false"
>
<span class="icon">{{ localeIcons[loc.code] }}</span>
<span class="name">{{ loc.name }}</span>
</NuxtLink>
</div>
</div>
</template>
<style scoped>
.language-switcher {
position: relative;
}
.current-lang {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
}
.dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
z-index: 50;
}
.lang-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
text-decoration: none;
color: inherit;
transition: background 0.2s;
}
.lang-option:hover {
background: #f3f4f6;
}
</style>
SEO 优化
自动 hreflang 标签
@nuxtjs/i18n 会自动生成 hreflang 标签:
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
baseUrl: 'https://example.com',
locales: [
{ code: 'zh', iso: 'zh-CN', name: '中文' },
{ code: 'en', iso: 'en-US', name: 'English' },
{ code: 'ja', iso: 'ja-JP', name: '日本語' }
]
}
})
生成的 HTML:
<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="ja-JP" href="https://example.com/ja/about">
<link rel="alternate" hreflang="x-default" href="https://example.com/about">
本地化的 Meta 信息
<!-- pages/index.vue -->
<script setup lang="ts">
const { t } = useI18n()
// 动态设置本地化的 SEO 信息
useSeoMeta({
title: t('pages.home.seo.title'),
description: t('pages.home.seo.description'),
ogTitle: t('pages.home.seo.title'),
ogDescription: t('pages.home.seo.description')
})
</script>
翻译文件:
// locales/zh.json
{
"pages": {
"home": {
"seo": {
"title": "HTMLPAGE - 让网页构建更简单",
"description": "专业的网页构建平台,提供模板市场、可视化编辑器、SEO优化工具,帮助您快速创建高质量网页。"
}
}
}
}
// locales/en.json
{
"pages": {
"home": {
"seo": {
"title": "HTMLPAGE - Make Web Building Easier",
"description": "Professional web building platform with template marketplace, visual editor, and SEO tools to help you create high-quality web pages quickly."
}
}
}
}
本地化 Sitemap
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n', '@nuxtjs/sitemap'],
sitemap: {
// Sitemap 会自动包含所有语言版本
autoI18n: true,
// 或者手动配置
sources: [
'/api/__sitemap__/urls'
]
}
})
日期、数字和货币格式化
配置格式化规则
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
// Vue I18n 配置
vueI18n: './i18n.config.ts'
}
})
// i18n.config.ts
export default defineI18nConfig(() => ({
legacy: false,
// 数字格式
numberFormats: {
zh: {
currency: {
style: 'currency',
currency: 'CNY',
notation: 'standard'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
},
percent: {
style: 'percent',
useGrouping: false
}
},
en: {
currency: {
style: 'currency',
currency: 'USD',
notation: 'standard'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
},
percent: {
style: 'percent',
useGrouping: false
}
},
ja: {
currency: {
style: 'currency',
currency: 'JPY',
notation: 'standard'
}
}
},
// 日期时间格式
datetimeFormats: {
zh: {
short: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
},
time: {
hour: '2-digit',
minute: '2-digit'
}
},
en: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
},
time: {
hour: 'numeric',
minute: '2-digit',
hour12: true
}
}
}
}))
在组件中使用格式化
<script setup lang="ts">
const { n, d } = useI18n()
const price = 1234.56
const date = new Date()
const percentage = 0.856
</script>
<template>
<div>
<!-- 货币格式 -->
<p>价格:{{ n(price, 'currency') }}</p>
<!-- 中文:¥1,234.56 -->
<!-- 英文:$1,234.56 -->
<!-- 小数格式 -->
<p>数量:{{ n(1234.5, 'decimal') }}</p>
<!-- 百分比格式 -->
<p>完成度:{{ n(percentage, 'percent') }}</p>
<!-- 输出:85.6% -->
<!-- 日期格式 -->
<p>日期:{{ d(date, 'short') }}</p>
<!-- 中文:2024/12/27 -->
<!-- 英文:Dec 27, 2024 -->
<p>详细日期:{{ d(date, 'long') }}</p>
<!-- 中文:2024年12月27日 星期五 -->
<!-- 英文:Friday, December 27, 2024 -->
</div>
</template>
懒加载翻译文件
对于大型应用,可以按需加载翻译文件以减少初始加载时间。
基础懒加载配置
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
lazy: true,
langDir: 'locales/',
locales: [
{ code: 'zh', file: 'zh.json' },
{ code: 'en', file: 'en.json' },
{ code: 'ja', file: 'ja.json' }
]
}
})
按模块分割翻译文件
locales/
├── zh/
│ ├── common.json # 通用翻译(导航、按钮等)
│ ├── home.json # 首页翻译
│ ├── products.json # 产品页翻译
│ └── account.json # 账户相关翻译
├── en/
│ ├── common.json
│ ├── home.json
│ ├── products.json
│ └── account.json
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
lazy: true,
langDir: 'locales/',
locales: [
{
code: 'zh',
files: [
'zh/common.json',
'zh/home.json',
'zh/products.json',
'zh/account.json'
]
},
{
code: 'en',
files: [
'en/common.json',
'en/home.json',
'en/products.json',
'en/account.json'
]
}
]
}
})
动态加载特定模块
// composables/usePageTranslations.ts
export async function usePageTranslations(page: string) {
const { locale, mergeLocaleMessage } = useI18n()
// 动态导入页面翻译
const translations = await import(`~/locales/${locale.value}/${page}.json`)
// 合并到当前翻译
mergeLocaleMessage(locale.value, { [page]: translations.default })
}
<!-- pages/products/index.vue -->
<script setup lang="ts">
// 加载产品页专用翻译
await usePageTranslations('products')
</script>
服务端翻译
在 API 路由中使用翻译
// server/api/products/index.ts
export default defineEventHandler(async (event) => {
// 从请求中获取语言
const locale = getHeader(event, 'accept-language')?.split(',')[0] || 'zh'
// 加载翻译
const messages = await loadTranslations(locale)
const products = await fetchProducts()
return products.map(product => ({
...product,
// 使用翻译的分类名称
categoryName: messages.categories[product.categoryId] || product.categoryId
}))
})
async function loadTranslations(locale: string) {
const { default: messages } = await import(`~/locales/${locale}.json`)
return messages
}
邮件模板国际化
// server/utils/email.ts
import { createI18n } from 'vue-i18n'
export async function sendLocalizedEmail(
to: string,
template: string,
locale: string,
data: Record<string, any>
) {
// 加载对应语言的邮件模板
const { default: messages } = await import(`~/locales/emails/${locale}.json`)
const i18n = createI18n({
locale,
messages: { [locale]: messages }
})
const t = i18n.global.t
const subject = t(`${template}.subject`, data)
const body = t(`${template}.body`, data)
await sendEmail({ to, subject, body })
}
TypeScript 支持
类型安全的翻译键
// types/i18n.d.ts
import type { DefineLocaleMessage } from 'vue-i18n'
// 导入基础翻译文件作为类型参考
import type zh from '~/locales/zh.json'
declare module 'vue-i18n' {
export interface DefineLocaleMessage {
common: typeof zh.common
pages: typeof zh.pages
components: typeof zh.components
}
}
declare module '@nuxtjs/i18n' {
export interface Locales {
zh: DefineLocaleMessage
en: DefineLocaleMessage
ja: DefineLocaleMessage
}
}
创建类型安全的翻译 Hook
// composables/useTypedI18n.ts
import type zh from '~/locales/zh.json'
type MessageSchema = typeof zh
// 递归获取所有键的路径
type DeepKeys<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? DeepKeys<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
: never
}[keyof T]
: never
type TranslationKey = DeepKeys<MessageSchema>
export function useTypedI18n() {
const { t, ...rest } = useI18n()
// 类型安全的 t 函数
const typedT = (key: TranslationKey, params?: Record<string, any>) => {
return t(key, params)
}
return {
t: typedT,
...rest
}
}
最佳实践
翻译文件组织原则
✅ 推荐结构:
locales/
├── zh.json # 按语言组织
├── en.json
└── ja.json
或者:
locales/
├── zh/ # 按语言+模块组织(大型项目)
│ ├── index.json # 入口文件
│ ├── common.json
│ └── pages/
│ ├── home.json
│ └── about.json
键命名规范
{
// ✅ 使用语义化命名
"button": {
"submit": "提交",
"cancel": "取消",
"confirm": "确认"
},
// ✅ 按页面/组件组织
"pages": {
"home": {
"title": "首页"
}
},
// ❌ 避免
"btn1": "提交", // 无意义的命名
"home_page_title": "首页" // 下划线风格不一致
}
避免硬编码
<!-- ❌ 避免 -->
<template>
<button>提交</button>
<p>共 3 件商品</p>
</template>
<!-- ✅ 推荐 -->
<template>
<button>{{ $t('button.submit') }}</button>
<p>{{ $t('cart.items', { count: 3 }) }}</p>
</template>
翻译缺失处理
// i18n.config.ts
export default defineI18nConfig(() => ({
legacy: false,
// 缺失翻译时的处理
missing: (locale, key) => {
console.warn(`[i18n] Missing translation: ${key} (${locale})`)
// 开发环境显示键名
if (process.dev) {
return `[${key}]`
}
// 生产环境返回空或降级内容
return ''
},
// 降级语言
fallbackLocale: 'zh'
}))
检查清单
项目配置
- 安装并配置 @nuxtjs/i18n
- 确定 URL 路由策略
- 配置语言检测和 Cookie
- 设置基础 URL(用于 hreflang)
翻译管理
- 建立统一的翻译文件结构
- 制定键命名规范
- 配置懒加载(大型项目)
- 设置 TypeScript 类型支持
SEO 优化
- 确认 hreflang 标签正确生成
- 每个页面有本地化的 title 和 description
- Sitemap 包含所有语言版本
用户体验
- 实现语言切换组件
- 配置日期、数字、货币格式化
- 处理翻译缺失的降级方案
总结
Nuxt 的 i18n 模块提供了完整的国际化解决方案:
- 路由策略:灵活的 URL 前缀配置
- 翻译管理:支持嵌套结构、参数、复数形式
- SEO 优化:自动 hreflang、本地化 Meta
- 格式化:日期、数字、货币的本地化显示
- 性能优化:翻译文件懒加载
国际化不仅是翻译文字,更是为不同文化背景的用户提供最佳体验。合理规划、规范实施,才能让你的应用真正走向国际。


