Astro 国际化重构:从平行目录树到参数化路由
背景
博客初期为了快速上线,中英文页面采用了平行目录树的简单方案:
src/pages/
zh/
index.astro
about.astro
archive.astro
en/
index.astro
about.astro
archive.astro
每新增一种语言就需要复制一整套页面文件,维护成本随语言数量线性增长。更严重的是,各页面中散落了语言推断逻辑——“当前是中文还是英文?“的答案散落在 Layout、导航组件、页面组件各处,没有统一出口。
重构目标
- 新增语言零成本——只需在配置中注册 + 添加翻译文件,无需复制页面文件
- 路径前缀统一管理——消除分散在各组件中的路径推导逻辑
- 翻译与路由完全解耦——路由逻辑不依赖翻译文本,翻译文本不依赖路由路径
新架构
目录结构
src/
i18n/
config.ts ← 语言配置中心
utils.ts ← t() 翻译函数 + basePath() 路径前缀
zh.json ← 中文翻译
en.json ← 英文翻译
routes.ts ← 统一路由出口
pages/
[lang]/ ← 参数化目录,同一文件处理所有语言
index.astro
about.astro
archive.astro
核心配置
src/i18n/config.ts 是整个 i18n 体系的单一配置源头:
export const locales = ['zh', 'en'] as const;
export type Locale = typeof locales[number];
export const defaultLocale: Locale = 'zh';
export const localePaths: Record<Locale, string> = {
zh: '', // 默认语言无前缀
en: 'en', // 非默认语言有前缀
};
统一路由层
src/routes.ts 导出所有路径计算逻辑:
import { localePaths } from './i18n/config';
export const home = (lang: Locale) => `/${localePaths[lang]}`;
export const about = (lang: Locale) => `/${localePaths[lang]}/about`;
export const archive = (lang: Locale) => `/${localePaths[lang]}/archive`;
所有组件只 import 不自己算,彻底消除了分散的路径推导。
翻译函数
import zh from './zh.json';
import en from './en.json';
const translations = { zh, en };
export function t(key: string, lang: Locale): string {
return translations[lang]?.[key] ?? translations['zh'][key] ?? key;
}
语言切换
语言切换按钮通过 HTML data 属性读取目标路径,消除客户端硬编码:
<a href="#" class="lang-toggle" id="lang-toggle"
data-lang-en="/en" data-lang-zh="/zh">EN</a>
新增语言流程
新增一个语言(如日语)只需:
- 在
config.ts的locales数组中加入'ja' - 在
localePaths中加入'ja': 'ja' - 在
src/i18n/下新增ja.json翻译文件
零页面文件复制。
旧问题修复
这次重构一并修复了遗留问题:
- RSS alternate link 硬编码
/en/feed.xml - 语言切换按钮 en→zh 路径丢失
- 各组件内散落的
window.location.pathname判断逻辑
总结
从平行目录树到参数化路由,本质上是将散落在各处的”局部知识”收拢到统一的配置层。这不仅降低了维护成本,更重要的是让新增语言变成了一个纯粹的数据操作,而非工程操作。