在Next.js中实现无默认语言前缀的多语言路由适配Spree Commerce
实现Next.js多语言路由(匹配WP/WPML/WooCommerce结构)
针对你的需求——默认语言(英语)无前缀,其他语言带对应前缀,且各语言路由使用独立翻译的slug,以下是具体实现方案:
核心配置:定义语言与路由映射
首先创建一个i18n.js配置文件,统一管理语言列表、默认语言和各页面/内容类型的多语言slug:
export const languages = { en: { name: 'English', prefix: '' }, de: { name: 'Deutsch', prefix: 'de' }, fr: { name: 'Français', prefix: 'fr' } }; export const defaultLang = 'en'; // 各内容类型的多语言slug映射 export const routeSlugs = { about: { en: 'about', de: 'uber', fr: 'a-propos-de-nous' }, contact: { en: 'contact-us', de: 'kontakt', fr: 'contact' }, blog: { en: 'blog', de: 'blog', fr: 'blog' }, women: { en: 'women', de: 'damen', fr: 'femmes' }, shirts: { en: 'shirts', de: 'shirts', fr: 'chemises' }, fashionGuides: { en: 'fashion-guides', de: 'mode-blog', fr: 'guides-de-mode' } };
方案一:Pages Router(基于exportPathMap)
适合使用Next.js 12及以下,或依赖静态导出的场景:
1. 配置next.config.js生成静态路由
在next.config.js中通过exportPathMap遍历语言和内容数据,生成所有符合要求的路由:
const { languages, defaultLang, routeSlugs } = require('./i18n'); module.exports = { async exportPathMap() { const pathMap = {}; // 生成首页路由 Object.keys(languages).forEach(lang => { const path = languages[lang].prefix ? `/${lang}` : '/'; pathMap[path] = { page: '/', query: { lang } }; }); // 生成关于页面及联系子页面 Object.keys(languages).forEach(lang => { const prefix = languages[lang].prefix; const aboutSlug = routeSlugs.about[lang]; const contactSlug = routeSlugs.contact[lang]; // 关于页面 const aboutPath = prefix ? `/${prefix}/${aboutSlug}` : `/${aboutSlug}`; pathMap[aboutPath] = { page: '/about', query: { lang } }; // 联系子页面 const contactPath = prefix ? `/${prefix}/${aboutSlug}/${contactSlug}` : `/${aboutSlug}/${contactSlug}`; pathMap[contactPath] = { page: '/about/contact', query: { lang } }; }); // 生成博客归档及单页(需替换为从Spree API获取的真实数据) const mockBlogPosts = [ { enSlug: 'meet-sarah-our-newest-ambassador', deSlug: 'treffen-sie-sarah-unsere-neueste-botschafterin', frSlug: 'rencontrer-sarah-notre-nouvelle-ambassadrice' } ]; Object.keys(languages).forEach(lang => { const prefix = languages[lang].prefix; const blogSlug = routeSlugs.blog[lang]; // 归档页 const archivePath = prefix ? `/${prefix}/${blogSlug}` : `/${blogSlug}`; pathMap[archivePath] = { page: '/blog', query: { lang } }; // 单页 mockBlogPosts.forEach(post => { const postPath = prefix ? `/${prefix}/${blogSlug}/${post[`${lang}Slug`]}` : `/${blogSlug}/${post[`${lang}Slug`]}`; pathMap[postPath] = { page: '/blog/[slug]', query: { lang, slug: post[`${lang}Slug`] } }; }); }); // 商品分类、子分类及商品页面(同理,从Spree API获取真实分类/商品数据) const mockProducts = [ { enSlug: 'green-blouse-shirt', deSlug: 'grunes-blusenshirt', frSlug: 'chemise-chemisier-verte' } ]; Object.keys(languages).forEach(lang => { const prefix = languages[lang].prefix; const womenSlug = routeSlugs.women[lang]; const shirtsSlug = routeSlugs.shirts[lang]; // 商品分类页 const womenPath = prefix ? `/${prefix}/${womenSlug}` : `/${womenSlug}`; pathMap[womenPath] = { page: '/products/category', query: { lang, categorySlug: womenSlug } }; // 子分类页 const shirtsPath = prefix ? `/${prefix}/${womenSlug}/${shirtsSlug}` : `/${womenSlug}/${shirtsSlug}`; pathMap[shirtsPath] = { page: '/products/category/[subcategory]', query: { lang, categorySlug: womenSlug, subcategorySlug: shirtsSlug } }; // 商品页 mockProducts.forEach(product => { const productPath = prefix ? `/${prefix}/${product[`${lang}Slug`]}` : `/${product[`${lang}Slug`]}`; pathMap[productPath] = { page: '/products/[slug]', query: { lang, slug: product[`${lang}Slug`] } }; }); }); // 自定义文章类型fashion-guides(归档+单页) const mockGuides = [ { enSlug: 'plus-size-fashion-trends-in-2023', deSlug: 'modetrends-in-ubergrossen-im-jahr-2023', frSlug: 'tendances-mode-grandes-tailles-en-2023' } ]; Object.keys(languages).forEach(lang => { const prefix = languages[lang].prefix; const fgSlug = routeSlugs.fashionGuides[lang]; // 归档页 const fgArchivePath = prefix ? `/${prefix}/${fgSlug}` : `/${fgSlug}`; pathMap[fgArchivePath] = { page: '/fashion-guides', query: { lang } }; // 单页 mockGuides.forEach(guide => { const guidePath = prefix ? `/${prefix}/${fgSlug}/${guide[`${lang}Slug`]}` : `/${fgSlug}/${guide[`${lang}Slug`]}`; pathMap[guidePath] = { page: '/fashion-guides/[slug]', query: { lang, slug: guide[`${lang}Slug`] } }; }); }); return pathMap; } };
2. 页面组件中处理语言与路由
以/pages/about.js为例,获取语言参数并渲染对应内容,同时生成正确的多语言链接:
import { useRouter } from 'next/router'; import { languages, routeSlugs } from '../i18n'; export default function AboutPage() { const router = useRouter(); const { lang } = router.query; // 从Spree API获取对应语言的关于页面内容 const aboutContent = getAboutContentByLang(lang); return ( <div> <h1>{aboutContent.title}</h1> <p>{aboutContent.content}</p> {/* 生成联系页面的本地化链接 */} <a href={getLocalizedUrl('contact', lang, { parent: 'about' })}> {aboutContent.contactLinkText} </a> {/* 语言切换按钮 */} <div className="lang-switcher"> {Object.keys(languages).map(langCode => ( <a key={langCode} href={getLocalizedUrl('about', langCode)}> {languages[langCode].name} </a> ))} </div> </div> ); } // 辅助函数:生成本地化URL function getLocalizedUrl(routeKey, targetLang, options = {}) { const prefix = languages[targetLang].prefix; let slug = routeSlugs[routeKey][targetLang]; if (options.parent) { slug = `${routeSlugs[options.parent][targetLang]}/${slug}`; } return prefix ? `/${prefix}/${slug}` : `/${slug}`; }
方案二:App Router(Next.js 13+)
适合使用Next.js 13及以上的新项目,利用App Router的动态路由和中间件能力:
1. 配置中间件处理语言路由
创建middleware.js,处理语言前缀的重定向和cookie设置:
import { NextResponse } from 'next/server'; import { languages, defaultLang } from './i18n'; export function middleware(request) { const { pathname } = request.nextUrl; const langMatch = pathname.match(/^\/([a-z]{2})\/?/); const currentLang = langMatch ? langMatch[1] : defaultLang; // 默认语言带/en前缀时,重定向到无前缀路径 if (currentLang === defaultLang && pathname.startsWith(`/${defaultLang}`)) { const newPath = pathname.replace(`/${defaultLang}`, '') || '/'; return NextResponse.redirect(new URL(newPath, request.url)); } // 非默认语言无前缀时,可根据需求重定向或返回404(这里保持原路径,由组件处理) const response = NextResponse.next(); response.cookies.set('lang', currentLang); return response; } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
2. 创建动态路由页面
以博客归档页为例:
- 英语归档页:
app/blog/page.js
export default function EnglishBlogArchive() { // 获取英语博客内容 const content = getBlogArchiveContent('en'); return <div>{content}</div>; }
- 多语言归档页:
app/[lang]/blog/page.js
import { languages } from '../../../i18n'; import { getBlogArchiveContent } from '../../../lib/spree'; // 生成所有非默认语言的路由参数 export async function generateStaticParams() { return Object.keys(languages) .filter(lang => lang !== defaultLang) .map(lang => ({ lang })); } export default function BlogArchive({ params }) { const { lang } = params; const content = getBlogArchiveContent(lang); return <div>{content}</div>; }
3. 商品页面动态路由
- 英语商品页:
app/[slug]/page.js
import { getProductBySlugAndLang } from '../../lib/spree'; export async function generateStaticParams() { const products = await getProductsFromSpree(); return products.map(product => ({ slug: product.enSlug })); } export default function EnglishProductPage({ params }) { const { slug } = params; const product = await getProductBySlugAndLang(slug, 'en'); return <div>{product.name}</div>; }
- 多语言商品页:
app/[lang]/[productSlug]/page.js
import { languages } from '../../../i18n'; import { getProductBySlugAndLang, getProductsFromSpree } from '../../../lib/spree'; export async function generateStaticParams() { const products = await getProductsFromSpree(); const params = []; Object.keys(languages).forEach(lang => { if (lang === 'en') return; products.forEach(product => { params.push({ lang, productSlug: product[`${lang}Slug`] }); }); }); return params; } export default function ProductPage({ params }) { const { lang, productSlug } = params; const product = await getProductBySlugAndLang(productSlug, lang); return <div>{product.name}</div>; }
Spree Commerce集成要点
- 从Spree API获取多语言数据:利用Spree的多语言支持,请求时指定
locale参数,或获取translations关联数据来提取各语言的slug和内容。 - 封装API调用函数:创建
lib/spree.js统一处理Spree API请求,比如:
export async function getProductBySlugAndLang(slug, lang) { const res = await fetch(`${process.env.SPREE_API_URL}/products?filter[slug]=${slug}&locale=${lang}`); const data = await res.json(); return data.data[0]; } export async function getProductsFromSpree() { const res = await fetch(`${process.env.SPREE_API_URL}/products?include=translations`); const data = await res.json(); return data.data.map(product => { const slugs = {}; product.relationships.translations.data.forEach(trans => { const translation = data.included.find(item => item.id === trans.id); slugs[translation.attributes.locale] = translation.attributes.slug; }); return { id: product.id, ...slugs }; }); }
注意事项
- SEO优化:为每个语言页面添加
hreflang标签,帮助搜索引擎识别多语言内容。 - 404处理:创建自定义404页面,处理不存在的多语言路由。
- 语言切换:确保切换语言时跳转到对应页面的翻译路径,而非仅添加语言前缀。
内容的提问来源于stack exchange,提问作者johnnyjohn
相关产品推荐
相关产品推荐

