工程
使用 Astro i18n 构建多语言站点
深入解析 Astro 内置 i18n 路由机制 — prefixDefaultLocale、getStaticPaths、语言感知组件的设计思路与实践。
为什么 Astro 的 i18n 方案更合理
大多数框架的多语言支持是事后拼接上去的。Astro 将其作为一等功能,只需在 astro.config.mjs 中配置 i18n 块,即可完成语言定义、路由策略和回退行为的全部设置。
// astro.config.mjs
i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'zh-TW', 'en', 'ja', 'fr', 'es', 'de'],
routing: {
prefixDefaultLocale: true, // 默认语言也加前缀
},
}
启用 prefixDefaultLocale: true 后,所有语言(包括默认的 zh-CN)都会获得 /locale/ 前缀。最直接的收益是:不再需要同时维护根目录页面和 [lang]/ 页面两套代码。
getStaticPaths 基本模式
需要在所有语言下生成同一页面时,通用模板如下:
---
import { supportedLocales } from '../i18n/index';
export function getStaticPaths() {
return supportedLocales.map(lang => ({ params: { lang } }));
}
const { lang } = Astro.params;
const lp = `/${lang}`; // 用于构建 href 的语言前缀
---
对于文章、视频等内容页面,需要将语言列表与内容条目做笛卡尔积:
export async function getStaticPaths() {
const posts = await getCollection('articles');
return supportedLocales.flatMap(lang =>
posts
.filter(p => !p.data.locale || p.data.locale === lang) // 语言专属过滤
.map(post => ({ params: { lang, slug: post.id }, props: { post } }))
);
}
语言专属内容
有时某篇文章只希望在特定语言下出现——比如语言专属教程、文化相关内容,或者(就像本文一系列测试文章!)用于验证 i18n 路由效果。
在内容集合 Schema 中添加可选的 locale 字段:
// content.config.ts
locale: z.string().optional(),
在 frontmatter 中声明:
---
title: "仅日文专属文章"
locale: "ja" # 只会出现在 /ja/articles 下
---
列表页自动过滤:
const posts = (await getCollection('articles'))
.filter(p => !p.data.locale || p.data.locale === lang);
特别注意:默认语言(zh-CN)如果希望展示所有文章(包括其他语言专属的),可以跳过 locale 过滤:
.filter(p => lang === 'zh-CN' || !p.data.locale || p.data.locale === lang)
组件中的语言感知
凡是需要生成 href 的组件,都要知道当前语言前缀。有两种方式:
1. 使用 Astro.currentLocale(共享组件)
const _lp = `/${Astro.currentLocale ?? 'zh-CN'}`;
// href={`${_lp}/articles`} → /zh-CN/articles、/en/articles 等
2. 从 Props 接收(页面级组件)
const { lang } = Astro.props;
const lp = `/${lang}`;
关键认知:启用 prefixDefaultLocale: true 后,默认语言不再需要特殊处理。以前的条件判断:
const _lp = locale === 'zh-CN' ? '' : `/${locale}`;
可以彻底删掉,统一改为:
const _lp = `/${locale}`;
语言切换器的 JavaScript 实现
切换器只需先去除当前语言前缀,再加上目标语言前缀即可:
const ALL_LOCALES = ['zh-CN', 'zh-TW', 'en', 'ja', 'fr', 'es', 'de'];
function resolveLocaleHref(targetLocale: string): string {
const path = window.location.pathname;
let base = path;
for (const loc of ALL_LOCALES) {
const prefix = `/${loc}`;
if (path.startsWith(`${prefix}/`) || path === prefix) {
base = path.slice(prefix.length) || '/';
break;
}
}
return `/${targetLocale}${base === '/' ? '/' : base}`;
}
逻辑对称,没有任何特殊情况,七种语言完全统一处理。
总结对比
变更前(prefixDefaultLocale: false) | 变更后(prefixDefaultLocale: true) |
|---|---|
根目录页面 + [lang]/ 页面双重维护 | 只维护 [lang]/ 页面 |
zh-CN 需要特殊处理(无前缀) | 所有语言对称一致 |
_lp 需要条件判断 | 统一使用 /${locale} |
| 语言切换器有特殊分支 | 纯通用逻辑 |
这次架构调整的投入,会在日后每一次新增语言、新增页面时持续节省维护成本。