← 返回日志

Astro 全局 i18n 统一范式规范.

我们当前在 Astro 项目中建立并完善的这套 i18n 范式,核心思想是“入口严格净化收窄、内部类型单向流动、消费端极轻量降级”。

这套规范彻底摒弃了传统的二元硬编码(isZh / isEn)、重型救火工具(getLocalized)以及组件内部散落的条件分支。

一、 核心基础设计(src/i18n/ui.ts)

定义最清晰的基础类型与全局收窄 Helper 函数:

TypeScript

// 1. 系统支持的所有语言字典定义
export const languages = {
  zh: '简体中文',
  en: 'English',
  ja: '日本語',
  de: 'Deutsch',
  es: 'Español',
} as const;

// 2. 导出标准的语言 Keys 类型 ("zh" | "en" | "ja" | "de" | "es")
export type SupportedLanguage = keyof typeof languages;

// 3. 提取所有支持的语言 Key 数组,方便运行时精准校验
export const supportedLangKeys = Object.keys(languages) as SupportedLanguage[];

// 4. 兜底默认语言
export const defaultLang: SupportedLanguage = 'zh';

// 5. 宽松输入类型(仅用于组件 Props / Astro.params 接口输入,允许接受未知字符串)
export type AcceptableLanguage = SupportedLanguage | (string & {});

// 6. 核心净化收窄函数(全站统一入口)
export function toSupportedLang(lang?: unknown): SupportedLanguage {
  if (typeof lang === 'string' && supportedLangKeys.includes(lang as SupportedLanguage)) {
    return lang as SupportedLanguage;
  }
  return defaultLang;
}

二、 三大基本开发原则

1. 入口第一行:收窄类型 (Type Narrowing)

所有组件/页面的 Frontmatter 代码第一句,必须将传入的 rawLang 净化为严格的 SupportedLanguage:

Code snippet

---
import { defaultLang, toSupportedLang, type AcceptableLanguage } from "@/i18n/ui";

interface Props {
  lang?: AcceptableLanguage; // 对外暴露宽松类型接口
}

const { lang: rawLang } = Astro.props; // 或 Astro.params

// 💡 必须在第一步完成收窄,后续变量 lang 的类型锁定为 SupportedLanguage
const lang = toSupportedLang(rawLang);
---

2. 消费端取值:内联极轻量 pick() 函数

无需复杂工具函数,消费端只需定义一个极简的泛型 pick,既能提取内联字典,又能安全读取 JSON 节点,并享受自动 fallback:

TypeScript

// 通用安全提取 Helper(支持可选链与多层 Fallback)
function pick<T>(dict: Partial<Record<string, T>> | undefined, currentLang: string): T | undefined {
  if (!dict) return undefined;
  return dict[currentLang] ?? dict[defaultLang] ?? Object.values(dict)[0];
}

3. 路由与 HTML 模板:完全解耦硬编码

  • 动态路由:统一采用模板字符串 /${lang}/your-path/ 拼接 URL,确保在任何语种下点击链接都能停留在当前语种路由(不再强制重定向到 /en/ 或 /zh/)。
  • 模板文案:直接调用 {pick({ zh: "...", en: "...", ja: "..." }, lang)} 替代内联三元表达式(isZh ? ... : ...)。

三、 JSON 数据与 TypeScript 严格模式规范

当提取 JSON 中的复杂结构(如数组列表 .map())时,必须通过 TypeScript 接口为 pick 显式标注泛型类型,并在 DOM 渲染处添加可选链(?.),彻底消除 Parameter 'item' implicitly has an 'any' type 及 Object is possibly 'undefined' 报错:

Code snippet

---
interface CapabilityItem {
  number: string;
  title: string;
  description: string;
}

interface CapabilityData {
  items: CapabilityItem[];
}

// 显式指定泛型,精准推导返回类型
const capabilities = pick<CapabilityData>(capabilitiesData.capabilitiesFloor, lang);
---

<div>
  {/* 使用 ?. 防止数据节点缺失导致的运行时报错 */}
  {capabilities?.items?.map((item) => (
    <div>
      <h4>{item.title}</h4>
      <p>{item.description}</p>
    </div>
  ))}
</div>

四、 架构收益总结

  1. 类型零死角:组件外部允许传入任意字符串(AcceptableLanguage),经过 toSupportedLang() 洗净后,组件内部 100% 运行在严格的 SupportedLanguage 类型下。
  2. 多语种无限扩展:未来若要增加法语(fr)或德语(de),只需在 languages 字典和 JSON 中补充对应节点,所有页面及底层组件自动适配,无需改动任何条件分支代码。
  3. 彻底去除技术债务:清理了 isZh 等过渡判断变量,废弃并删除了冗余复杂的 getLocalized 函数,大幅降低了项目的代码体积与维护成本。

相关文章 / 延续阅读 →