为什么 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}
语言切换器有特殊分支纯通用逻辑

这次架构调整的投入,会在日后每一次新增语言、新增页面时持续节省维护成本。