教程
多语言内容示例
同一篇文章,在不同语言下显示不同内容,但路由相同。通过文件夹结构实现 i18n 内容翻译的最佳实践。
为什么需要多语言同路由
当你想让 /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 | 路由会出错 |
文件夹方案是三者中最直观、最不容易出错的。