背景

博客初期为了快速上线,中英文页面采用了平行目录树的简单方案:

src/pages/
  zh/
    index.astro
    about.astro
    archive.astro
  en/
    index.astro
    about.astro
    archive.astro

每新增一种语言就需要复制一整套页面文件,维护成本随语言数量线性增长。更严重的是,各页面中散落了语言推断逻辑——“当前是中文还是英文?“的答案散落在 Layout、导航组件、页面组件各处,没有统一出口。

重构目标

  1. 新增语言零成本——只需在配置中注册 + 添加翻译文件,无需复制页面文件
  2. 路径前缀统一管理——消除分散在各组件中的路径推导逻辑
  3. 翻译与路由完全解耦——路由逻辑不依赖翻译文本,翻译文本不依赖路由路径

新架构

目录结构

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>

新增语言流程

新增一个语言(如日语)只需:

  1. config.tslocales 数组中加入 'ja'
  2. localePaths 中加入 'ja': 'ja'
  3. src/i18n/ 下新增 ja.json 翻译文件

零页面文件复制

旧问题修复

这次重构一并修复了遗留问题:

  • RSS alternate link 硬编码 /en/feed.xml
  • 语言切换按钮 en→zh 路径丢失
  • 各组件内散落的 window.location.pathname 判断逻辑

总结

从平行目录树到参数化路由,本质上是将散落在各处的”局部知识”收拢到统一的配置层。这不仅降低了维护成本,更重要的是让新增语言变成了一个纯粹的数据操作,而非工程操作。