Nuxt 国际化(i18n)多语言方案完全指南

HTMLPAGE 团队
20分钟 分钟阅读

深入讲解 Nuxt 3 应用的国际化实现方案,涵盖 @nuxtjs/i18n 模块配置、翻译文件管理、语言切换、SEO 优化、日期货币格式化、懒加载策略,以及大型项目的最佳实践。

#Nuxt #i18n #国际化 #多语言 #Vue I18n

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 模块提供了完整的国际化解决方案:

  1. 路由策略:灵活的 URL 前缀配置
  2. 翻译管理:支持嵌套结构、参数、复数形式
  3. SEO 优化:自动 hreflang、本地化 Meta
  4. 格式化:日期、数字、货币的本地化显示
  5. 性能优化:翻译文件懒加载

国际化不仅是翻译文字,更是为不同文化背景的用户提供最佳体验。合理规划、规范实施,才能让你的应用真正走向国际。