Next.js纯URL参数国际化

半兽人 发表于: 2025-07-22   最后更新时间: 2025-07-23 17:41:50  
{{totalSubscript}} 订阅, 61 游览

基于 Next.js 15 的国际化项目,只使用URL参数(如 ?hi=zh)实现语言切换。

项目特性

SEO友好

  • 通过URL参数控制语言,如 /apps?hi=zh 显示中文版
  • 完整的meta标签和hreflang
  • 支持搜索引擎索引

快速开始

安装依赖

pnpm install

启动开发服务器

pnpm dev

访问页面

  • 英文版:http://localhost:3000/apps
  • 中文版:http://localhost:3000/apps?hi=zh

项目结构

next-base/
├── app/
│   ├── apps/                    # 示例页面(纯参数控制)
│   │   └── page.tsx            # 使用 simpleUrlRedirect
│   ├── components/
│   │   └── LanguageSwitcher.tsx # 语言切换器(纯参数)
│   └── layout.tsx              # 根布局
├── i18n/                       # 国际化配置
│   ├── index.ts               # 语言配置
│   ├── server.ts              # 服务端翻译
│   ├── client.ts              # 客户端翻译
│   ├── en-US/                 # 英文翻译
│   └── zh-Hans/               # 中文翻译
├── lib/
│   └── urlRedirect.ts         # URL参数处理工具
└── README.md                  # 项目说明

使用示例

基本用法

// app/apps/page.tsx
import { simpleUrlRedirect } from '@/lib/urlRedirect'
import { getTranslation } from '@/i18n/server'
import LanguageSwitcher from '@/app/components/LanguageSwitcher'
import { Metadata } from 'next'
import type { Locale } from '@/i18n'

interface AppsPageProps {
    searchParams: Promise<Record<string, string | string[] | undefined>>;
}

export default async function AppsPage({ searchParams }: AppsPageProps) {
    // 一行代码搞定URL参数处理!
    const locale = await simpleUrlRedirect(searchParams) as Locale

    const { t: tCommon } = await getTranslation(locale, 'common')
    const { t: tApps } = await getTranslation(locale, 'apps')

    return (
        <div className="p-8 max-w-4xl mx-auto">
            <div className="flex justify-between items-center mb-6">
                <div>
                    <h1 className="text-3xl font-bold">{tApps('title')}</h1>
                    <p className="text-gray-600 mt-2">{tApps('description')}</p>
                </div>
                <LanguageSwitcher />
            </div>
            {/* 页面内容 */}
        </div>
    )
}

语言切换组件

// /app/components/LanguageSwitcher.tsx
import { simpleUrlRedirect } from '@/lib/urlRedirect'
import { getTranslation } from '@/i18n/server'
import LanguageSwitcher from '@/app/components/LanguageSwitcher'
import { Metadata } from 'next'
import type { Locale } from '@/i18n'

interface AppsPageProps {
    searchParams: Promise<Record<string, string | string[] | undefined>>;
}

export default async function AppsPage({ searchParams }: AppsPageProps) {
    // 一行代码搞定URL参数处理!
    const locale = await simpleUrlRedirect(searchParams) as Locale

    const { t: tCommon } = await getTranslation(locale, 'common')
    const { t: tApps } = await getTranslation(locale, 'apps')

    return (
        <div className="p-8 max-w-4xl mx-auto">
            <div className="flex justify-between items-center mb-6">
                <div>
                    <h1 className="text-3xl font-bold">{tApps('title')}</h1>
                    <p className="text-gray-600 mt-2">{tApps('description')}</p>
                </div>
                <LanguageSwitcher />
            </div>
            {/* 页面内容 */}
        </div>
    )
}

LanguageSwitcher

'use client'

import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { i18n, setLocaleOnClient, type Locale } from '@/i18n'

export default function LanguageSwitcher() {
    const router = useRouter()
    const pathname = usePathname()
    const searchParams = useSearchParams()
    const [isOpen, setIsOpen] = useState(false)

    // 从URL参数获取当前语言
    const getCurrentLocale = (): string => {
        const hi = searchParams.get('hi')
        return hi === 'zh' ? 'zh-Hans' : 'en-US'
    }

    const switchLanguage = async (locale: string) => {
        // 设置语言偏好到 Cookie
        setLocaleOnClient(locale as Locale, false)

        // 更新 URL 参数
        const newSearchParams = new URLSearchParams(searchParams)

        if (locale === 'en-US') {
            // 英文版:移除hi参数
            newSearchParams.delete('hi')
        } else {
            // 中文版:设置hi=zh参数
            newSearchParams.set('hi', 'zh')
        }

        const newUrl = newSearchParams.toString()
            ? `${pathname}?${newSearchParams.toString()}`
            : pathname

        // 使用 push 而不是 refresh,避免无限循环
        router.push(newUrl)
        setIsOpen(false)
    }

    const currentLocale = getCurrentLocale()

    return (
        <div className="relative">
            <button
                onClick={() => setIsOpen(!isOpen)}
                className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
            >
                <span>🌐 {currentLocale === 'en-US' ? 'English' : '中文'}</span>
                <svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                </svg>
            </button>

            {isOpen && (
                <div className="absolute top-full mt-2 right-0 bg-white border border-gray-200 rounded-lg shadow-lg z-50 min-w-[150px]">
                    {i18n.locales.map((locale) => (
                        <button
                            key={locale}
                            onClick={() => switchLanguage(locale)}
                            className={`w-full text-left px-4 py-2 hover:bg-gray-100 transition-colors first:rounded-t-lg last:rounded-b-lg ${currentLocale === locale ? 'bg-blue-50 text-blue-600' : ''
                                }`}
                        >
                            {locale === 'en-US' ? '🇺🇸 English' : '🇨🇳 中文'}
                        </button>
                    ))}
                </div>
            )}
        </div>
    )
}

smartUrlRedirect / simpleUrlRedirect

import { redirect } from 'next/navigation'
import { useTranslation } from '@/i18n/server'

/**
 * 智能URL参数处理函数
 * 1. 优先使用URL参数
 * 2. 如果没有参数,检测用户语言偏好并自动跳转
 * 
 * @param searchParams 搜索参数
 * @param currentPath 当前路径
 */
export async function smartUrlRedirect(
    searchParams: Promise<Record<string, string | string[] | undefined>>,
    currentPath: string
) {
    const params = await searchParams
    const hi = params.hi

    // 1. 如果用户主动选择了语言参数,直接返回对应语言
    if (hi === 'zh') {
        return 'zh-Hans'
    }
    if (hi === 'en') {
        return 'en-US'
    }

    // 2. 如果没有参数,检测用户语言偏好
    const detectedLocale = await useTranslation()

    // 3. 如果检测到中文用户,自动跳转到中文版
    // 注意:只有在没有 hi 参数时才跳转,避免无限循环
    if (detectedLocale === 'zh-Hans' && !hi) {
        redirect(`${currentPath}?hi=zh`)
    }

    // 4. 默认返回英文
    return 'en-US'
}

/**
 * 简化的URL参数处理函数 - 只处理参数,不跳转路径
 * 
 * @param searchParams 搜索参数
 */
export async function simpleUrlRedirect(
    searchParams: Promise<Record<string, string | string[] | undefined>>
) {
    const params = await searchParams
    const hi = params.hi

    // 如果检测到中文参数,返回中文语言标识
    if (hi === 'zh') {
        return 'zh-Hans'
    }

    // 默认返回英文
    return 'en-US'
}

英文数据集

// /app/i18n/en-US/common.ts
export default {
    welcome: 'Welcome',
    apps: 'Apps',
    home: 'Home',
    about: 'About',
    contact: 'Contact',
    language: 'Language',
    english: 'English',
    chinese: 'Chinese',
} as const
// /app/i18n/en-US/apps.ts
export default {
    title: 'Applications',
    description: 'Browse and manage your applications',
    features: {
        title: 'Features',
        list: [
            'Easy to use interface',
            'Fast performance',
            'Secure and reliable',
            '24/7 support'
        ]
    },
    stats: {
        total: 'Total Applications',
        active: 'Active',
        inactive: 'Inactive'
    },
    actions: {
        create: 'Create New App',
        edit: 'Edit',
        delete: 'Delete',
        view: 'View Details'
    }
} as const

中文数据集

// /app/i18n/zh-Hans/common.ts
export default {
    welcome: '欢迎',
    apps: '应用',
    home: '首页',
    about: '关于',
    contact: '联系',
    language: '语言',
    english: '英文',
    chinese: '中文',
} as const
// /app/i18n/zh-Hans/apps.ts
export default {
    title: '应用程序',
    description: '浏览和管理您的应用程序',
    features: {
        title: '功能特性',
        list: [
            '简单易用的界面',
            '快速性能',
            '安全可靠',
            '24/7 支持'
        ]
    },
    stats: {
        total: '应用程序总数',
        active: '活跃',
        inactive: '非活跃'
    },
    actions: {
        create: '创建新应用',
        edit: '编辑',
        delete: '删除',
        view: '查看详情'
    }
} as const

Demo

// /app/apps/page.tsx
import { simpleUrlRedirect } from '@/lib/urlRedirect'
import { getTranslation } from '@/i18n/server'
import LanguageSwitcher from '@/app/components/LanguageSwitcher'
import { Metadata } from 'next'
import type { Locale } from '@/i18n'

interface AppsPageProps {
    searchParams: Promise<Record<string, string | string[] | undefined>>;
}

// 生成元数据
export async function generateMetadata(): Promise<Metadata> {
    return {
        title: 'Applications',
        description: 'Manage your applications',
        openGraph: {
            title: 'Applications',
            description: 'Manage your applications',
        },
        alternates: {
            canonical: '/apps',
            languages: {
                'en': '/apps',
                'zh': '/apps?hi=zh',
                'x-default': '/apps',
            },
        },
    }
}

export default async function AppsPage({ searchParams }: AppsPageProps) {
    // 使用简化的URL参数处理 - 不跳转路径,只返回语言
    const locale = await simpleUrlRedirect(searchParams) as Locale

    const { t: tCommon } = await getTranslation(locale, 'common')
    const { t: tApps } = await getTranslation(locale, 'apps')

    // 结构化数据
    const structuredData = {
        "@context": "https://schema.org",
        "@type": "WebPage",
        "name": tApps('title'),
        "description": tApps('description'),
        "inLanguage": locale,
        "url": `https://yourdomain.com/apps`,
        "alternateName": {
            "en": "Applications",
            "zh": "应用程序"
        }
    }

    return (
        <>
            <script
                type="application/ld+json"
                dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
            />

            {/* 添加 hreflang 标签 */}
            <link rel="alternate" hrefLang="en" href="/apps" />
            <link rel="alternate" hrefLang="zh" href="/apps?hi=zh" />
            <link rel="alternate" hrefLang="x-default" href="/apps" />

            <div className="p-8 max-w-4xl mx-auto">
                <div className="flex justify-between items-center mb-6">
                    <div>
                        <h1 className="text-3xl font-bold">{tApps('title')}</h1>
                        <p className="text-gray-600 mt-2">{tApps('description')}</p>
                    </div>
                    <LanguageSwitcher />
                </div>

                <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
                    {/* 统计卡片 */}
                    <div className="bg-white p-6 rounded-lg shadow-md">
                        <h3 className="text-lg font-semibold mb-4">{tApps('stats.total')}</h3>
                        <div className="space-y-2">
                            <div className="flex justify-between">
                                <span>{tApps('stats.active')}:</span>
                                <span className="font-bold text-green-600">12</span>
                            </div>
                            <div className="flex justify-between">
                                <span>{tApps('stats.inactive')}:</span>
                                <span className="font-bold text-gray-600">3</span>
                            </div>
                        </div>
                    </div>

                    {/* 功能特性 */}
                    <div className="bg-white p-6 rounded-lg shadow-md">
                        <h3 className="text-lg font-semibold mb-4">{tApps('features.title')}</h3>
                        <ul className="space-y-2">
                            {tApps('features.list.0') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.0')}
                            </li>}
                            {tApps('features.list.1') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.1')}
                            </li>}
                            {tApps('features.list.2') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.2')}
                            </li>}
                            {tApps('features.list.3') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.3')}
                            </li>}
                        </ul>
                    </div>
                </div>

                {/* 操作按钮 */}
                <div className="bg-white p-6 rounded-lg shadow-md">
                    <h3 className="text-lg font-semibold mb-4">操作</h3>
                    <div className="flex flex-wrap gap-3">
                        <button className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
                            {tApps('actions.create')}
                        </button>
                        <button className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors">
                            {tApps('actions.edit')}
                        </button>
                        <button className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
                            {tApps('actions.view')}
                        </button>
                        <button className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
                            {tApps('actions.delete')}
                        </button>
                    </div>
                </div>

                {/* 语言链接 */}
                <div className="mt-6 p-4 bg-blue-50 rounded-lg">
                    <h3 className="text-lg font-medium mb-2">其他语言版本:</h3>
                    <div className="flex gap-4">
                        <a href="/apps" className="text-blue-600 hover:underline">🏠 默认 (英文)</a>
                        <a href="/apps?hi=zh" className="text-blue-600 hover:underline">🇨🇳 中文</a>
                    </div>
                </div>

                {/* 调试信息 */}
                <div className="mt-8 p-4 bg-gray-100 rounded-lg">
                    <h3 className="text-lg font-medium mb-2">调试信息:</h3>
                    <p><strong>当前语言:</strong> {locale}</p>
                    <p><strong>URL路径:</strong> /apps</p>
                    <p><strong>页面标题:</strong> {tApps('title')}</p>
                    <p><strong>欢迎信息:</strong> {tCommon('welcome')}</p>
                </div>
            </div>
        </>
    )
}

效果展示

Next.js纯URL参数国际化

URL示例

URL 行为 结果
/apps 显示英文版 英文内容
/apps?hi=zh 显示中文版 中文内容
/apps?hi=en 显示英文版 英文内容

添加新页面

  1. 复制现有页面模板

    // app/newpage/page.tsx
    import { simpleUrlRedirect } from '@/lib/urlRedirect'
    import { getTranslation } from '@/i18n/server'
    import LanguageSwitcher from '@/app/components/LanguageSwitcher'
    import { Metadata } from 'next'
    import type { Locale } from '@/i18n'
    
    interface NewPageProps {
        searchParams: Promise<Record<string, string | string[] | undefined>>;
    }
    
    export default async function NewPage({ searchParams }: NewPageProps) {
        // 一行代码搞定!
        const locale = await simpleUrlRedirect(searchParams) as Locale
    
        const { t: tCommon } = await getTranslation(locale, 'common')
        const { t: tApps } = await getTranslation(locale, 'apps')
    
        return (
            <div className="p-8 max-w-4xl mx-auto">
                <div className="flex justify-between items-center mb-6">
                    <div>
                        <h1 className="text-3xl font-bold">New Page</h1>
                        <p className="text-gray-600 mt-2">Your new page</p>
                    </div>
                    <LanguageSwitcher />
                </div>
                {/* 页面内容 */}
            </div>
        )
    }
    
  2. 访问方式

    • 英文版:/newpage
    • 中文版:/newpage?hi=zh

技术栈

  • 框架: Next.js 15 (App Router)
  • 语言: TypeScript
  • 样式: Tailwind CSS
  • 国际化: i18next + react-i18next
  • 包管理: pnpm

核心优势

1. 极简代码

  • 新增页面只需复制模板
  • 逻辑清晰,易于理解
  • 不需要复杂的路径映射

2. 高度可扩展

  • 轻松添加新页面
  • 支持自定义参数
  • 类型安全

3. SEO友好

  • 参数化URL支持
  • 完整meta标签
  • 搜索引擎优化

开发指南

添加新语言

  1. i18n/index.ts 中添加语言配置
  2. 创建对应的翻译文件
  3. 更新参数处理逻辑

自定义参数

  1. 修改 simpleUrlRedirect 函数
  2. 添加新的参数处理
  3. 更新文档

性能优化

  1. 翻译资源懒加载
  2. 缓存策略优化
  3. 静态生成优化

最佳实践

1. 保持一致性

  • 所有页面使用相同的参数化模式
  • 统一使用 simpleUrlRedirect 函数
  • 保持文件结构一致

2. 参数命名

  • 使用简洁明了的参数名(如 hi
  • 避免与现有参数冲突
  • 考虑SEO影响

3. 测试覆盖

  • 测试不同参数组合
  • 测试语言切换逻辑
  • 测试SEO元数据

这个项目展示了如何使用纯URL参数实现最简洁、最独立、最易维护的国际化解决方案。

更新于 2025-07-23

查看NextJS更多相关的文章或提一个关于NextJS的问题,也可以与我们一起分享文章