Nuxt 构建多语言官网系统完全指南

HTMLPAGE 团队
28分钟 分钟阅读

系统讲解使用 Nuxt 3 构建多语言国际化官网的完整方案,包括 i18n 配置、内容翻译管理、SEO 优化、语言切换等核心功能的实现。

#Nuxt #多语言 #国际化 #官网开发 #i18n

多语言官网需求分析

构建多语言官网需要解决以下核心问题:

  1. 语言切换机制 - 用户如何在不同语言版本间切换
  2. URL 策略 - 不同语言版本的 URL 如何设计
  3. 内容管理 - 如何高效管理多语言内容
  4. SEO 优化 - 让搜索引擎正确索引各语言版本
  5. 用户体验 - 自动检测用户语言偏好

本文将以一个完整的企业官网为例,详细讲解实现方案。

项目初始化

创建 Nuxt 项目

# 创建项目
npx nuxi init company-website
cd company-website

# 安装 i18n 模块
npm install @nuxtjs/i18n

# 安装其他必要依赖
npm install @nuxt/content @nuxt/image

基础配置

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/i18n',
    '@nuxt/content',
    '@nuxt/image'
  ],
  
  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/',
    
    // URL 策略
    strategy: 'prefix_except_default',
    
    // 检测用户语言
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      redirectOn: 'root'
    }
  }
})

目录结构

company-website/
├── locales/              # 翻译文件
│   ├── zh.json
│   ├── en.json
│   └── ja.json
├── content/              # Markdown 内容
│   ├── zh/
│   │   ├── about.md
│   │   └── services/
│   ├── en/
│   │   ├── about.md
│   │   └── services/
│   └── ja/
│       ├── about.md
│       └── services/
├── pages/
│   ├── index.vue
│   ├── about.vue
│   └── services/
│       └── [slug].vue
└── components/
    ├── LanguageSwitcher.vue
    └── Navigation.vue

翻译管理

翻译文件结构

将翻译内容按功能模块组织:

// locales/zh.json
{
  "common": {
    "home": "首页",
    "about": "关于我们",
    "services": "服务",
    "contact": "联系我们",
    "learnMore": "了解更多",
    "getStarted": "立即开始"
  },
  "home": {
    "hero": {
      "title": "领先的企业解决方案",
      "subtitle": "助力企业数字化转型,实现业务增长",
      "cta": "免费咨询"
    },
    "features": {
      "title": "为什么选择我们",
      "items": {
        "fast": {
          "title": "高效交付",
          "description": "敏捷开发流程,快速响应市场需求"
        },
        "reliable": {
          "title": "稳定可靠",
          "description": "99.9% 服务可用性保障"
        },
        "support": {
          "title": "专业支持",
          "description": "7×24小时技术支持服务"
        }
      }
    }
  },
  "footer": {
    "copyright": "© 2024 公司名称 版权所有",
    "privacy": "隐私政策",
    "terms": "服务条款"
  }
}
// locales/en.json
{
  "common": {
    "home": "Home",
    "about": "About Us",
    "services": "Services",
    "contact": "Contact",
    "learnMore": "Learn More",
    "getStarted": "Get Started"
  },
  "home": {
    "hero": {
      "title": "Leading Enterprise Solutions",
      "subtitle": "Empowering digital transformation for business growth",
      "cta": "Free Consultation"
    },
    "features": {
      "title": "Why Choose Us",
      "items": {
        "fast": {
          "title": "Fast Delivery",
          "description": "Agile development for rapid market response"
        },
        "reliable": {
          "title": "Reliable",
          "description": "99.9% service availability guarantee"
        },
        "support": {
          "title": "Professional Support",
          "description": "24/7 technical support service"
        }
      }
    }
  },
  "footer": {
    "copyright": "© 2024 Company Name. All rights reserved.",
    "privacy": "Privacy Policy",
    "terms": "Terms of Service"
  }
}

在模板中使用

<template>
  <div class="hero-section">
    <h1>{{ $t('home.hero.title') }}</h1>
    <p>{{ $t('home.hero.subtitle') }}</p>
    <button>{{ $t('home.hero.cta') }}</button>
  </div>
  
  <section class="features">
    <h2>{{ $t('home.features.title') }}</h2>
    
    <div class="feature-grid">
      <div 
        v-for="key in ['fast', 'reliable', 'support']" 
        :key="key"
        class="feature-card"
      >
        <h3>{{ $t(`home.features.items.${key}.title`) }}</h3>
        <p>{{ $t(`home.features.items.${key}.description`) }}</p>
      </div>
    </div>
  </section>
</template>

动态翻译(带参数)

// locales/zh.json
{
  "user": {
    "greeting": "你好,{name}!",
    "itemCount": "共 {count} 件商品",
    "lastLogin": "上次登录:{date}"
  }
}
<template>
  <div>
    <!-- 基础参数 -->
    <p>{{ $t('user.greeting', { name: user.name }) }}</p>
    
    <!-- 数字格式化 -->
    <p>{{ $t('user.itemCount', { count: items.length }) }}</p>
    
    <!-- 日期格式化 -->
    <p>{{ $t('user.lastLogin', { date: formatDate(lastLogin) }) }}</p>
  </div>
</template>

<script setup>
const { locale } = useI18n()

const formatDate = (date) => {
  return new Intl.DateTimeFormat(locale.value, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(new Date(date))
}
</script>

语言切换组件

基础下拉选择器

<!-- components/LanguageSwitcher.vue -->
<template>
  <div class="language-switcher">
    <button 
      class="current-lang"
      @click="isOpen = !isOpen"
    >
      <span>{{ currentLocale.name }}</span>
      <IconChevron :class="{ rotated: isOpen }" />
    </button>
    
    <Transition name="dropdown">
      <ul v-if="isOpen" class="lang-list">
        <li 
          v-for="locale in availableLocales" 
          :key="locale.code"
        >
          <NuxtLink
            :to="switchLocalePath(locale.code)"
            @click="isOpen = false"
          >
            {{ locale.name }}
          </NuxtLink>
        </li>
      </ul>
    </Transition>
  </div>
</template>

<script setup>
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const isOpen = ref(false)

const currentLocale = computed(() => 
  locales.value.find(l => l.code === locale.value)
)

const availableLocales = computed(() => 
  locales.value.filter(l => l.code !== locale.value)
)

// 点击外部关闭
onMounted(() => {
  document.addEventListener('click', (e) => {
    if (!e.target.closest('.language-switcher')) {
      isOpen.value = false
    }
  })
})
</script>

<style scoped>
.language-switcher {
  position: relative;
}

.current-lang {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 0.5rem;
  background: white;
  cursor: pointer;
}

.lang-list {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 0.5rem;
  padding: 0.5rem;
  background: white;
  border-radius: 0.5rem;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
  list-style: none;
}

.lang-list a {
  display: block;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  color: #374151;
  text-decoration: none;
}

.lang-list a:hover {
  background: #f3f4f6;
}

.dropdown-enter-active,
.dropdown-leave-active {
  transition: all 0.2s;
}

.dropdown-enter-from,
.dropdown-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}
</style>

内容多语言管理

使用 Nuxt Content

针对长文本内容(如博客、服务介绍),使用 Nuxt Content 管理 Markdown 文件:

<!-- content/zh/services/web-development.md -->
---
title: 网站开发服务
description: 专业的网站开发服务,从设计到上线一站式解决方案
image: /images/services/web-dev.jpg
---

## 服务概述

我们提供全栈网站开发服务,涵盖从需求分析到部署上线的完整流程。

### 技术栈

- **前端**: Vue.js, React, Nuxt
- **后端**: Node.js, Python, Go
- **数据库**: PostgreSQL, MongoDB

### 服务流程

1. 需求调研
2. 原型设计
3. 技术开发
4. 测试验收
5. 部署上线
<!-- content/en/services/web-development.md -->
---
title: Web Development Services
description: Professional web development from design to deployment
image: /images/services/web-dev.jpg
---

## Service Overview

We provide full-stack web development services covering the complete workflow.

### Tech Stack

- **Frontend**: Vue.js, React, Nuxt
- **Backend**: Node.js, Python, Go
- **Database**: PostgreSQL, MongoDB

### Service Process

1. Requirements Analysis
2. Prototype Design
3. Development
4. Testing & QA
5. Deployment

内容查询组件

<!-- pages/services/[slug].vue -->
<template>
  <article class="service-page">
    <div class="hero">
      <NuxtImg :src="data.image" :alt="data.title" />
      <h1>{{ data.title }}</h1>
      <p class="description">{{ data.description }}</p>
    </div>
    
    <ContentRenderer :value="data" class="content" />
  </article>
</template>

<script setup>
const { locale } = useI18n()
const route = useRoute()

// 根据当前语言查询对应内容
const { data } = await useAsyncData(
  `service-${locale.value}-${route.params.slug}`,
  () => queryContent(`/${locale.value}/services/${route.params.slug}`).findOne()
)

// 设置页面 SEO
useHead({
  title: data.value?.title,
  meta: [
    { name: 'description', content: data.value?.description }
  ]
})
</script>

SEO 多语言优化

hreflang 标签

自动为所有语言版本生成 hreflang 标签:

<!-- layouts/default.vue -->
<script setup>
const { locale, locales } = useI18n()
const route = useRoute()

// 生成所有语言版本的 hreflang
const hreflangLinks = computed(() => {
  const links = locales.value.map(loc => ({
    rel: 'alternate',
    hreflang: loc.iso,
    href: `https://example.com${loc.code === 'zh' ? '' : `/${loc.code}`}${route.path.replace(`/${locale.value}`, '')}`
  }))
  
  // 添加 x-default
  links.push({
    rel: 'alternate',
    hreflang: 'x-default',
    href: `https://example.com${route.path.replace(`/${locale.value}`, '')}`
  })
  
  return links
})

useHead({
  link: hreflangLinks
})
</script>

结构化数据多语言

// composables/useSchemaOrg.js
export const useSchemaOrg = () => {
  const { locale, t } = useI18n()
  const route = useRoute()
  
  const generateOrganizationSchema = () => ({
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: t('company.name'),
    description: t('company.description'),
    url: `https://example.com${locale.value === 'zh' ? '' : `/${locale.value}`}`,
    logo: 'https://example.com/logo.png',
    sameAs: [
      'https://twitter.com/company',
      'https://linkedin.com/company/company'
    ],
    contactPoint: {
      '@type': 'ContactPoint',
      telephone: t('company.phone'),
      contactType: 'customer service',
      availableLanguage: [locale.value]
    }
  })
  
  const generateWebPageSchema = (title, description) => ({
    '@context': 'https://schema.org',
    '@type': 'WebPage',
    name: title,
    description: description,
    inLanguage: locale.value,
    isPartOf: {
      '@type': 'WebSite',
      name: t('company.name'),
      url: 'https://example.com'
    }
  })
  
  return {
    generateOrganizationSchema,
    generateWebPageSchema
  }
}

多语言 Sitemap

// nuxt.config.ts
export default defineNuxtConfig({
  sitemap: {
    // 为每种语言生成独立的 sitemap
    sitemaps: {
      'sitemap-zh': {
        includeAppSources: true,
        filter: ({ url }) => !url.startsWith('/en') && !url.startsWith('/ja')
      },
      'sitemap-en': {
        includeAppSources: true,
        filter: ({ url }) => url.startsWith('/en'),
        urls: () => {
          // 可以动态生成 URL
          return [
            { loc: '/en/', changefreq: 'daily', priority: 1 },
            { loc: '/en/about', changefreq: 'monthly', priority: 0.8 }
          ]
        }
      },
      'sitemap-ja': {
        includeAppSources: true,
        filter: ({ url }) => url.startsWith('/ja')
      }
    }
  }
})

导航与路由

多语言导航

<!-- components/Navigation.vue -->
<template>
  <nav class="main-nav">
    <div class="logo">
      <NuxtLink :to="localePath('/')">
        <img src="/logo.svg" alt="Logo" />
      </NuxtLink>
    </div>
    
    <ul class="nav-links">
      <li v-for="item in navItems" :key="item.key">
        <NuxtLink 
          :to="localePath(item.path)"
          :class="{ active: isActive(item.path) }"
        >
          {{ $t(`common.${item.key}`) }}
        </NuxtLink>
      </li>
    </ul>
    
    <div class="nav-actions">
      <LanguageSwitcher />
      <NuxtLink :to="localePath('/contact')" class="cta-btn">
        {{ $t('common.contact') }}
      </NuxtLink>
    </div>
  </nav>
</template>

<script setup>
const localePath = useLocalePath()
const route = useRoute()

const navItems = [
  { key: 'home', path: '/' },
  { key: 'about', path: '/about' },
  { key: 'services', path: '/services' }
]

const isActive = (path) => {
  if (path === '/') {
    return route.path === '/' || route.path.match(/^\/(en|ja)?$/)
  }
  return route.path.includes(path)
}
</script>

程序化导航

// 切换语言并导航
const switchAndNavigate = (targetLocale, targetPath) => {
  const localePath = useLocalePath()
  navigateTo(localePath(targetPath, targetLocale))
}

// 获取当前页面的其他语言版本 URL
const getAlternateUrl = (targetLocale) => {
  const switchLocalePath = useSwitchLocalePath()
  return switchLocalePath(targetLocale)
}

日期与货币格式化

日期本地化

// composables/useLocaleFormat.js
export const useLocaleFormat = () => {
  const { locale } = useI18n()
  
  const formatDate = (date, options = {}) => {
    const defaultOptions = {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    }
    
    return new Intl.DateTimeFormat(
      locale.value,
      { ...defaultOptions, ...options }
    ).format(new Date(date))
  }
  
  const formatRelativeTime = (date) => {
    const rtf = new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' })
    const diff = Date.now() - new Date(date).getTime()
    
    const seconds = Math.floor(diff / 1000)
    const minutes = Math.floor(seconds / 60)
    const hours = Math.floor(minutes / 60)
    const days = Math.floor(hours / 24)
    
    if (days > 0) return rtf.format(-days, 'day')
    if (hours > 0) return rtf.format(-hours, 'hour')
    if (minutes > 0) return rtf.format(-minutes, 'minute')
    return rtf.format(-seconds, 'second')
  }
  
  const formatCurrency = (amount, currency = 'CNY') => {
    // 根据语言选择默认货币
    const currencyMap = {
      zh: 'CNY',
      en: 'USD',
      ja: 'JPY'
    }
    
    return new Intl.NumberFormat(locale.value, {
      style: 'currency',
      currency: currencyMap[locale.value] || currency
    }).format(amount)
  }
  
  return {
    formatDate,
    formatRelativeTime,
    formatCurrency
  }
}

使用示例

<template>
  <div class="pricing-card">
    <h3>{{ plan.name }}</h3>
    <div class="price">
      {{ formatCurrency(plan.price) }}
      <span class="period">/{{ $t('pricing.month') }}</span>
    </div>
    <p class="updated">
      {{ $t('common.lastUpdated') }}: {{ formatDate(plan.updatedAt) }}
    </p>
  </div>
</template>

<script setup>
const { formatDate, formatCurrency } = useLocaleFormat()
</script>

开发与调试

翻译缺失提示

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    // 开发环境显示缺失的翻译 key
    missingWarn: process.env.NODE_ENV === 'development',
    fallbackWarn: process.env.NODE_ENV === 'development'
  }
})

翻译管理工具

创建辅助脚本检查翻译完整性:

// scripts/check-translations.js
const fs = require('fs')
const path = require('path')

const localesDir = path.join(__dirname, '../locales')
const locales = ['zh', 'en', 'ja']

// 读取所有翻译文件
const translations = {}
locales.forEach(locale => {
  const filePath = path.join(localesDir, `${locale}.json`)
  translations[locale] = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
})

// 提取所有 key
const getAllKeys = (obj, prefix = '') => {
  return Object.keys(obj).reduce((keys, key) => {
    const fullKey = prefix ? `${prefix}.${key}` : key
    if (typeof obj[key] === 'object') {
      return [...keys, ...getAllKeys(obj[key], fullKey)]
    }
    return [...keys, fullKey]
  }, [])
}

// 以中文为基准检查其他语言
const baseKeys = new Set(getAllKeys(translations.zh))

locales.filter(l => l !== 'zh').forEach(locale => {
  const localeKeys = new Set(getAllKeys(translations[locale]))
  
  const missing = [...baseKeys].filter(k => !localeKeys.has(k))
  const extra = [...localeKeys].filter(k => !baseKeys.has(k))
  
  if (missing.length) {
    console.log(`\n❌ ${locale} 缺少的翻译:`)
    missing.forEach(k => console.log(`   - ${k}`))
  }
  
  if (extra.length) {
    console.log(`\n⚠️ ${locale} 多余的翻译:`)
    extra.forEach(k => console.log(`   - ${k}`))
  }
  
  if (!missing.length && !extra.length) {
    console.log(`\n✅ ${locale} 翻译完整`)
  }
})

部署与性能

静态生成

多语言网站推荐使用静态生成:

// nuxt.config.ts
export default defineNuxtConfig({
  // 静态生成
  ssr: true,
  
  nitro: {
    prerender: {
      // 预渲染所有语言版本
      crawlLinks: true,
      routes: [
        '/',
        '/en',
        '/ja',
        '/about',
        '/en/about',
        '/ja/about'
      ]
    }
  }
})

翻译文件按需加载

对于大型应用,避免一次性加载所有翻译:

// 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' }
    ]
  }
})

总结

构建多语言官网的核心要点:

  1. 架构设计 - 使用 prefix 策略管理 URL,便于 SEO
  2. 翻译管理 - 模块化组织翻译文件,便于维护
  3. 内容分离 - 短文本用 JSON,长内容用 Markdown
  4. SEO 完善 - hreflang、多语言 sitemap、结构化数据
  5. 用户体验 - 自动检测语言,提供便捷切换

遵循这些原则,可以构建出专业的多语言国际化官网。