在为几内亚等地区搭建无障碍指南网站(IWAC项目)时,页面内容通常需要同时支持法语、英语和本地语言(如富拉语、马宁卡语、索索语)。如果语言标识只是简单的string类型,任何拼写失误都会被编译器放过,最终在线上表现为页面语言切换失败或翻译缺失。用TypeScript在类型层面把这些约束固化下来,是最直接的解决思路。

一、定义语言代码的联合类型
首先需要明确项目支持哪些语言。几内亚的官方语言是法语,英语常用于国际访问者,本地语言则服务于视障用户使用读屏软件时的母语需求。用一个字面量联合类型把这些语言代码固定下来:
// 支持的语言代码,ISO 639-1 标准
export type SupportedLocale = 'fr' | 'en' | 'ff' | 'man' | 'suso';
// 语言显示名称映射
export const LOCALE_NAMES: Record<SupportedLocale, string> = {
fr: 'Français',
en: 'English',
ff: 'Fulfulde',
man: 'Maninka',
suso: 'Sosoxi',
};
这里的关键在于使用Record<SupportedLocale, string>而不是普通的索引签名。前者会在编译期强制你为每一种语言提供显示名称,新增语言时如果忘记补充,编译直接报错。如果写成{ [key: string]: string },那么任何拼写错误都能通过,约束就形同虚设了。
另一个细节是默认语言的处理。无障碍规范建议为页面指定默认语言,可以用类型交叉的方式扩展:
export interface IwacConfig {
defaultLocale: SupportedLocale;
fallbackLocale: SupportedLocale;
locales: readonly SupportedLocale[];
}
export const iwacConfig: IwacConfig = {
defaultLocale: 'fr',
fallbackLocale: 'en',
locales: ['fr', 'en', 'ff', 'man', 'suso'],
};
注意locales用了readonly修饰,避免运行时被意外修改。这个配置对象是整个类型体系的锚点,后续所有语言相关函数都从它推导类型。
二、翻译资源的类型结构设计
指南页面由多个区块组成,比如导航栏、指南正文、无障碍声明、操作提示等。翻译资源应该按页面结构组织,并保证各语言文件结构完全一致:
// 页面区块的键
export type GuideSection =
| 'navigation'
| 'intro'
| 'screenReaderTips'
| 'keyboardNavigation'
| 'contact';
// 每个区块内的文案键
export interface SectionKeys {
title: string;
description: string;
ctaLabel?: string; // 部分区块才有按钮文案
}
// 单个语言的完整翻译表
export type TranslationMap = Record<GuideSection, SectionKeys>;
// 所有语言的资源集合
export type TranslationBundle = Record<SupportedLocale, TranslationMap>;
这种结构带来的好处是强一致性校验。假设法语资源文件里写了screanReaderTips(拼写错误),TypeScript会立刻标红,因为GuideSection联合类型中不存在这个键。同样,如果某个语言的翻译文件遗漏了intro区块,Record<SupportedLocale, TranslationMap>会强制报出缺失项,不需要依赖人工核对。
对于从后端或翻译平台动态加载的情况,可以先定义宽松类型,再用类型守卫收窄:
function isTranslationMap(data: unknown): data is TranslationMap {
if (typeof data !== 'object' || data === null) return false;
const sections = ['navigation', 'intro', 'screenReaderTips',
'keyboardNavigation', 'contact'];
return sections.every(s => s in data);
}
async function loadLocale(locale: SupportedLocale): Promise<TranslationMap> {
const res = await fetch(`/locales/${locale}.json`);
const data: unknown = await res.json();
if (!isTranslationMap(data)) {
throw new Error(`Locale ${locale} 资源结构不完整`);
}
return data;
}
三、语言与路由及无障碍属性的类型关联
多语言站点的URL通常带语言前缀,比如/fr/guide/screen-reader。把路由解析函数的返回值与语言类型绑定,可以杜绝解析出未知语言的情况:
interface LocalizedRoute {
locale: SupportedLocale;
path: string;
}
const LOCALE_PREFIX = /^\/(fr|en|ff|man|suso)(\/.*)?$/;
function parseRoute(url: string): LocalizedRoute | null {
const match = url.match(LOCALE_PREFIX);
if (!match) return null;
return {
locale: match[1] as SupportedLocale,
path: match[2] ?? '/',
};
}
无障碍方面,HTML的<html>标签必须携带正确的lang属性,读屏软件依赖它切换发音引擎。可以为设置语言属性的操作封装一个类型安全的函数:
function setDocumentLocale(locale: SupportedLocale): void {
document.documentElement.lang = locale;
// 富拉语和马宁卡语使用扩展子标签标明书写系统
if (locale === 'ff') {
document.documentElement.lang = 'ff-Latn-GN';
}
}
函数签名只接受SupportedLocale,任何传入'french'或'FR '(多余空格)的调用都无法通过编译。相比在整个代码库里搜索字符串字面量,这种约束把排查成本降到接近零。
四、泛型工具与构建期完整性检查
最后可以补充几个泛型工具类型,进一步提升可维护性。比如提取某个区块在所有语言下的文案,用于翻译平台的对账:
// 提取指定区块的所有翻译
type SectionTranslations<S extends GuideSection> = {
[L in SupportedLocale]: TranslationBundle[L][S];
};
// 编译期完整性检查:如果某个常量类型为 never,说明资源缺失
type _CheckCompleteness = TranslationBundle extends
Record<SupportedLocale, TranslationMap> ? true : never;
const completenessCheck: _CheckCompleteness = true;
再配合一个语言切换器的React组件签名示例,展示类型如何贯穿UI层:
interface LocaleSwitcherProps {
current: SupportedLocale;
onSwitch: (locale: SupportedLocale) => void;
}
总结来看,这套方案的核心是用联合类型收窄语言代码取值,用Record强制翻译资源完整性,用类型守卫处理动态数据,再让lang属性设置和路由解析都依赖同一个类型定义。所有语言相关的不一致都能在编译阶段暴露,这对无障碍网站尤其重要——读屏用户对翻译缺失的容忍度远低于普通用户,一次静默的语言切换失败就可能让他们完全无法获取页面内容。
TypeScript类型定义国际化i18n修改时间:2026-09-05 01:34:36