为什么需要多语言同路由

当你想让 /zh-CN/articles/multilingual-example/en/articles/multilingual-example/ja/articles/multilingual-example 分别显示对应语言的内容,同时保持 slug 完全一致时,文件夹方案是最简洁的实现。

文件夹结构方案

只需将文件按语言放入对应子文件夹,无需任何额外字段:

src/content/articles/
  zh-CN/
    multilingual-example.mdx   # 仅 /zh-CN/ 显示
  en/
    multilingual-example.mdx   # 仅 /en/ 显示
  ja/
    multilingual-example.mdx   # 仅 /ja/ 显示
  some-universal-article.mdx   # 所有语言都显示

规则:

  • 根目录文件 → 所有语言都显示(可用 locale 字段指定特定语言)
  • 语言子文件夹 → 文件夹名决定语言,locale 字段失效

核心实现

两个辅助函数放在 src/i18n/index.ts

// 解析 ID 中的语言前缀
export function parseContentId(id: string): { folderLocale: Locale | null; baseId: string } {
  for (const locale of supportedLocales) {
    if (id.startsWith(`${locale}/`)) {
      return { folderLocale: locale, baseId: id.slice(locale.length + 1) };
    }
  }
  return { folderLocale: null, baseId: id };
}

// 判断是否应在当前语言下显示
export function contentMatchesLocale(
  id: string,
  frontmatterLocale: string | undefined,
  lang: Locale,
): boolean {
  const { folderLocale } = parseContentId(id);
  if (folderLocale !== null) return folderLocale === lang;   // 文件夹优先
  return !frontmatterLocale || frontmatterLocale === lang;   // 回退到 locale 字段
}

getStaticPaths 中使用:

export async function getStaticPaths() {
  const posts = await getCollection('articles');
  return supportedLocales.flatMap((lang) =>
    posts
      .filter(post => contentMatchesLocale(post.id, post.data.locale, lang))
      .map((post) => ({
        params: { lang, slug: parseContentId(post.id).baseId },  // 去掉语言前缀
        props: { post },
      }))
  );
}

效果对比

文件路径生成的路由
articles/zh-CN/multilingual-example.mdx/zh-CN/articles/multilingual-example
articles/en/multilingual-example.mdx/en/articles/multilingual-example
articles/ja/multilingual-example.mdx/ja/articles/multilingual-example
articles/universal.mdx/zh-CN/articles/universal + /en/articles/universal + …

与旧方案对比

方案如何指定语言是否需要额外字段
文件夹方案(新)文件所在目录名
frontmatter locale 字段locale: "en"
文件名后缀(废弃)article.en.mdx路由会出错

文件夹方案是三者中最直观、最不容易出错的。