FusionKit 国际化最佳实践:i18next + react-i18next(含 i18n Ally 友好配置)
QiuYeDx
2026-01-21
12 分钟
这篇文章基于 FusionKit 的最新国际化重构实践,目标是让你可以“复制即用”,快速搭建一个符合最佳实践、可维护、并且对 i18n Ally 等编辑器插件友好的国际化项目。
目标与原则
- 规范化目录:语言与命名空间清晰分离,便于组织与拆分。
- 强可读性:显式资源注册,避免“魔法加载”导致工具无法解析。
- 稳定持久化:用户语言选择稳定保存,且避免边界异常。
- 一致的调用方式:前端组件、非组件逻辑都使用同一 i18n 实例。
目录结构(推荐)
1src/2 locales/3 zh/4 common.json5 home.json6 tools.json7 about.json8 setting.json9 subtitle.json10 en/11 ...12 ja/13 ...14 i18n/15 constants.ts16 resources.ts17 index.ts命名空间(namespace)按页面/模块拆分,common.json 作为默认命名空间。
1) 常量统一:语言、命名空间、持久化 Key
src/i18n/constants.ts
Ts
1import { LangEnum } from "@/type/lang";2
3export const LANGUAGE_STORAGE_KEY = "lang";4export const DEFAULT_LANGUAGE = LangEnum.ZH;5export const FALLBACK_LANGUAGE = LangEnum.ZH;6export const SUPPORTED_LANGUAGES = Object.values(LangEnum) as LangEnum[];7
8export const NAMESPACES = [9 "common",10 "home",11 "tools",12 "about",13 "setting",14 "subtitle",15] as const;16
17export type Namespace = (typeof NAMESPACES)[number];18export const DEFAULT_NAMESPACE: Namespace = "common";19
20export const normalizeLanguage = (lng?: string | null): LangEnum => {21 if (!lng) return DEFAULT_LANGUAGE;22 const base = lng.split("-")[0] as LangEnum;23 return SUPPORTED_LANGUAGES.includes(base) ? base : DEFAULT_LANGUAGE;24};25
26export const resolveInitialLanguage = (): LangEnum => {27 if (typeof window === "undefined") return DEFAULT_LANGUAGE;28 try {29 const stored = localStorage.getItem(LANGUAGE_STORAGE_KEY);30 return normalizeLanguage(stored);31 } catch {32 return DEFAULT_LANGUAGE;33 }34};要点:
normalizeLanguage处理zh-CN/en-US之类的地区语言。- 所有命名空间显式声明,避免 “动态扫描” 隐式推断。
2) 资源显式注册(i18n Ally 友好)
src/i18n/resources.ts
Ts
1import type { Resource } from "i18next";2import { LangEnum } from "@/type/lang";3
4import enAbout from "@/locales/en/about.json";5import enCommon from "@/locales/en/common.json";6import enHome from "@/locales/en/home.json";7import enSetting from "@/locales/en/setting.json";8import enSubtitle from "@/locales/en/subtitle.json";9import enTools from "@/locales/en/tools.json";10
11import jaAbout from "@/locales/ja/about.json";12import jaCommon from "@/locales/ja/common.json";13import jaHome from "@/locales/ja/home.json";14import jaSetting from "@/locales/ja/setting.json";15import jaSubtitle from "@/locales/ja/subtitle.json";16import jaTools from "@/locales/ja/tools.json";17
18import zhAbout from "@/locales/zh/about.json";19import zhCommon from "@/locales/zh/common.json";20import zhHome from "@/locales/zh/home.json";21import zhSetting from "@/locales/zh/setting.json";22import zhSubtitle from "@/locales/zh/subtitle.json";23import zhTools from "@/locales/zh/tools.json";24
25export const resources: Resource = {26 [LangEnum.EN]: {27 common: enCommon,28 home: enHome,29 tools: enTools,30 about: enAbout,31 setting: enSetting,32 subtitle: enSubtitle,33 },34 [LangEnum.JA]: {35 common: jaCommon,36 home: jaHome,37 tools: jaTools,38 about: jaAbout,39 setting: jaSetting,40 subtitle: jaSubtitle,41 },42 [LangEnum.ZH]: {43 common: zhCommon,44 home: zhHome,45 tools: zhTools,46 about: zhAbout,47 setting: zhSetting,48 subtitle: zhSubtitle,49 },50};为什么不使用 import.meta.glob?
- 动态加载的文件结构对 i18n Ally 不友好,插件无法静态解析真实资源。
- 显式导入能让工具和 IDE 更准确地读取、补全、跳转和预览。
3) i18n 初始化与持久化
src/i18n/index.ts
Ts
1import i18n from "i18next";2import { initReactI18next } from "react-i18next";3import { resources } from "./resources";4import {5 DEFAULT_NAMESPACE,6 FALLBACK_LANGUAGE,7 LANGUAGE_STORAGE_KEY,8 NAMESPACES,9 normalizeLanguage,10 resolveInitialLanguage,11 SUPPORTED_LANGUAGES,12} from "./constants";13
14const initialLanguage = resolveInitialLanguage();15
16i18n.use(initReactI18next).init({17 resources,18 lng: initialLanguage,19 fallbackLng: FALLBACK_LANGUAGE,20 supportedLngs: SUPPORTED_LANGUAGES,21 ns: NAMESPACES,22 defaultNS: DEFAULT_NAMESPACE,23 load: "languageOnly",24 interpolation: {25 escapeValue: false,26 },27 react: {28 useSuspense: false,29 },30});31
32i18n.on("languageChanged", (lng) => {33 if (typeof window === "undefined") return;34 try {35 localStorage.setItem(LANGUAGE_STORAGE_KEY, normalizeLanguage(lng));36 } catch {37 // 忽略存储失败38 }39});40
41export default i18n;要点:
supportedLngs和load: "languageOnly"可避免地区语言的误判。useSuspense: false避免默认 Suspense 行为引入额外处理。- 初始化与持久化逻辑集中,避免重复与不一致。
4) 入口统一加载(确保全局可用)
src/main.tsx
Ts
无论组件内还是非组件内(比如 store、工具函数)使用 i18n.t(...),都应从统一入口导入:import i18n from "@/i18n"。
5) 语言切换 Hook(统一状态)
src/hook/useLanguage.ts
Ts
1import { useState, useEffect, useCallback } from "react";2import i18n from "@/i18n";3import { LangEnum } from "@/type/lang";4import { normalizeLanguage } from "@/i18n/constants";5
6const useLanguage = () => {7 const [language, setLanguage] = useState<LangEnum>(() =>8 normalizeLanguage(i18n.resolvedLanguage || i18n.language)9 );10
11 useEffect(() => {12 const handleLanguageChange = (lng: string) => {13 setLanguage(normalizeLanguage(lng));14 };15
16 i18n.on("languageChanged", handleLanguageChange);17 return () => {18 i18n.off("languageChanged", handleLanguageChange);19 };20 }, []);21
22 const changeLanguage = useCallback((lng: LangEnum) => {23 i18n.changeLanguage(lng);24 }, []);25
26 return { language, changeLanguage };27};28
29export default useLanguage;要点:
- Hook 不直接触碰本地存储,避免重复状态源。
- 以 i18n 事件为唯一可信来源。
6) i18n Ally 插件配置(关键)
.vscode/settings.json
JSON
必须确保:
pathMatcher与真实文件结构一致。namespace与keystyle设置一致(本项目是嵌套结构)。
7) 组件内使用规范
Ts
建议始终使用 namespace:key 格式,避免不同命名空间的 key 冲突。
常见坑与规避
- 动态 import 导致 i18n Ally 失效:改成显式导入资源。
- 未声明命名空间:插件无法判断所有可用 key。
- 多处初始化 i18n:统一入口,避免实例不一致。
- 语言值不标准:通过
normalizeLanguage统一处理。
快速复用清单
- 规范目录结构
src/locales/{locale}/{namespace}.json - 统一常量与命名空间
src/i18n/constants.ts - 显式资源注册
src/i18n/resources.ts - 初始化入口
src/i18n/index.ts - 入口注入
src/main.tsx -
useLanguage统一状态 - i18n Ally 配置齐全
结语
这套结构能显著提升可维护性,同时让工具链(尤其是 i18n Ally)稳定工作。如果你未来新增语言或命名空间,只需新增 JSON 文件并在 resources.ts 中注册即可,不需要额外改动初始化逻辑。