重点一:如何拆分 namespace#
官方文档强调 “按 locale 组织消息 + nested 结构”,但没有给出多文件拆分的落地方式。可维护的拆分方式如下:
-
目录按 locale、文件按 feature
messages/{locale}/{namespace}.json
每个功能一个 namespace 文件,避免单文件膨胀,团队协作时也更容易控制冲突。 -
配置驱动加载
i18n/config.ts维护locales与namespaces;请求入口一次性并行加载全部 namespace,再合并为完整messages。官方文档提到getRequestConfig在请求周期内缓存执行,这一层只做一次加载与合并。 -
默认语言兜底合并
默认语言补齐缺失项,既保证完整性,又保持 runtime 用法不变;getMessageFallback可用于缺失提示。
一个推荐的加载形态如下:
// i18n/request.ts
async function loadMessages(locale: Locale) {
const userMessages = await importNamespaces(locale);
if (locale === defaultLocale) return userMessages;
const baseMessages = await importNamespaces(defaultLocale);
return deepmerge(baseMessages, userMessages);
}
重点二:如何提供类型检查#
官方文档给出类型安全的总体方向,但需要补上 “怎么把 key 变成类型” 的落地步骤。可维护的做法有两种:
方案 A:Messages 类型 + module augmentation(最通用)
用基准语言的 JSON 生成 Messages 类型,再扩展 next-intl 的 AppConfig。这样 t("...") 只能使用真实存在的 key。
// types/i18n.d.ts
type CommonMessages = typeof import("../messages/en-US/common.json");
export type Messages = {
common: CommonMessages;
auth: typeof import("../messages/en-US/auth.json");
chat: typeof import("../messages/en-US/chat.json");
};
// types/global.d.ts
declare module "next-intl" {
interface AppConfig {
Locale: Locale;
Messages: Messages;
}
}
方案 B:createMessagesDeclaration(官方实验方案)
createNextIntlPlugin 的 createMessagesDeclaration 可以基于样例消息文件生成严格声明,进一步强化参数类型安全。
官方也指出 JSON 推断存在限制,因此生成声明文件是一条更可靠的路径。
小结#
拆分 namespace 解决规模与协作问题,类型约束解决 key 漏洞;统一入口加载与兜底合并保证运行时成本一致。这两件事做到位,i18n 才能长期稳定扩展。