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