You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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集成要点

  1. 从Spree API获取多语言数据:利用Spree的多语言支持,请求时指定locale参数,或获取translations关联数据来提取各语言的slug和内容。
  2. 封装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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 04:40:55