多语言官网需求分析
构建多语言官网需要解决以下核心问题:
- 语言切换机制 - 用户如何在不同语言版本间切换
- URL 策略 - 不同语言版本的 URL 如何设计
- 内容管理 - 如何高效管理多语言内容
- SEO 优化 - 让搜索引擎正确索引各语言版本
- 用户体验 - 自动检测用户语言偏好
本文将以一个完整的企业官网为例,详细讲解实现方案。
项目初始化
创建 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' }
]
}
})
总结
构建多语言官网的核心要点:
- 架构设计 - 使用 prefix 策略管理 URL,便于 SEO
- 翻译管理 - 模块化组织翻译文件,便于维护
- 内容分离 - 短文本用 JSON,长内容用 Markdown
- SEO 完善 - hreflang、多语言 sitemap、结构化数据
- 用户体验 - 自动检测语言,提供便捷切换
遵循这些原则,可以构建出专业的多语言国际化官网。


