Appearance
前端语言文件结构
本文档引用的文件
目录
项目结构分析
项目 pc
模块下的 src/locales
目录是前端多语言实现的核心区域,包含以下关键文件:
zh-cn.ts
:简体中文语言包en.ts
:英文语言包i18n.ts
:国际化逻辑处理与 API 调用封装index.ts
:语言环境初始化与默认语言选择main.ts
:Vue 应用入口,未直接集成 Vue I18n,而是采用自定义方案
该结构表明项目未使用标准的 vue-i18n
插件进行静态语言包管理,而是构建了一套基于服务器动态获取、本地缓存和按需加载的混合式国际化方案。
Section sources
语言文件组织结构
中文与英文语言文件
当前 zh-cn.ts
和 en.ts
文件内容为空对象:
ts
export default {
};
这表明所有用户界面文本并非通过静态语言包预定义,而是从后端数据库动态获取。语言键(code)不以内联方式存储于前端,而是通过 API 请求按需加载。
嵌套对象结构设计
虽然当前文件为空,但根据 index.ts
的合并逻辑,其设计支持嵌套对象结构:
ts
import elementZhLocale from "element-plus/es/locale/lang/zh-cn";
import zhLocale from "./zh-cn";
const messages = {
[elementZhLocale.name]: {
...zhLocale,
...elementZhLocale,
},
};
此模式允许:
- 模块化管理:将业务语言资源与第三方组件库(如 Element Plus)语言资源分离
- 动态扩展:运行时可合并多个语言源
- 覆盖机制:自定义语言可覆盖组件库默认文本
Section sources
i18n 初始化与配置
语言环境检测
getLocale()
函数实现智能语言检测:
ts
export function getLocale(): string {
let usr = { lang: undefined };
const usrStr = localStorage.getItem("usr");
if (usrStr) {
try {
usr = JSON.parse(usrStr);
} catch (e) { /* empty */ }
}
let lang = usr?.lang;
if (lang) return lang;
lang = navigator.language;
if (["zh", "zh-cn", "zh-hans", "zh-hans-cn"].indexOf(lang.toLowerCase()) > -1) {
lang = "zh-CN";
}
return lang;
}
检测优先级:
- 用户登录状态中保存的语言偏好
- 浏览器语言自动识别
- 默认 fallback 为
zh-CN
默认语言设置
通过 lang
常量导出当前语言,并在 messages
中查找对应语言包。若未找到则 fallback 到 zh-cn
。
Section sources
国际化使用方式
useI18n 组合式函数
useI18n()
是核心 API,返回一组翻译方法:
ts
export function useI18n(routePath?: string | null) {
const usrStore = useUsrStore();
const lang = usrStore.lang;
return {
n(code: string, ...args: any[]) { /* 路由绑定翻译 */ },
nAsync(code: string, ...args: any[]) { /* 异步翻译 */ },
initI18ns(codes: string[]) { /* 预加载多个code */ },
ns(code: string, ...args: any[]) { /* 全局系统级翻译 */ },
nsAsync(code: string, ...args: any[]) { /* 异步全局翻译 */ },
initSysI18ns(codes: string[]) { /* 预加载系统级语言 */ }
};
}
方法说明
方法名 | 用途 | 绑定路径 |
---|---|---|
n() | 同步翻译,绑定当前路由 | route.path + code |
nAsync() | 异步翻译 | route.path + code |
ns() | 系统级同步翻译 | "" + code |
nsAsync() | 系统级异步翻译 | "" + code |
参数替换支持
支持两种占位符语法:
{0}
、{1}
:按索引替换参数{name}
:按对象属性名替换
ts
// 示例
this.$t('Hello {0}, welcome to {site}', 'Alice', { site: 'OurApp' })
Section sources
动态加载与缓存机制
本地缓存管理
initI18nLblsLang()
负责初始化语言缓存:
ts
function initI18nLblsLang() {
const i18nsLangStr = localStorage.getItem(`i18nLblsLang`);
if (i18nsLangStr) {
try {
i18nLblsLang = JSON.parse(i18nsLangStr);
} catch (e) { }
}
// 版本校验
const __version = localStorage.getItem("__i18n_version");
if (i18nLblsLang?.__version !== __version) {
i18nLblsLang = undefined;
}
// 初始化当前语言空间
if (!i18nLblsLang) {
i18nLblsLang = { [lang]: {} };
}
}
缓存策略特点:
- 使用
localStorage
持久化存储 - 支持版本控制(
__i18n_version
) - 按语言分桶存储(
i18nLblsLang[lang]
)
懒加载机制
getLbl()
实现按需加载:
ts
function getLbl(lang, code, routePath, ...args) {
const key = `${routePath} ${code}`;
let i18nLbl = i18nLbls[key];
if (i18nLbl) {
return setLblArgs(i18nLbl, args);
}
// 缓存不存在,异步请求
(async function() {
i18nLbls[key] = await n0(lang, routePath, code, { notLoading: true });
localStorage.setItem(`i18nLblsLang`, JSON.stringify(i18nLblsLang));
})();
return setLblArgs(code, args); // 返回code作为临时文本
}
行为逻辑:
- 先查本地缓存
- 缓存命中 → 返回翻译文本
- 缓存未命中 → 后台异步请求,立即返回原始 code 作为占位符
- 请求完成后更新缓存并刷新响应式引用
流程图
Diagram sources
Section sources
类型安全与开发调试
类型安全方案
当前实现使用 any
类型,缺乏编译期类型检查:
ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
建议改进方向:
- 定义
I18nKey
联合类型枚举所有语言键 - 创建类型守卫函数确保 key 合法性
- 使用模块声明合并增强
useI18n
返回类型
开发环境调试技巧
启用/禁用动态加载
通过环境变量控制行为:
ts
if (import.meta.env.VITE_SERVER_I18N_ENABLE === "false") {
return setLblArgs(code, args); // 直接返回code
}
开发时可设置 VITE_SERVER_I18N_ENABLE=false
快速定位未翻译文本。
缓存清理
手动清除缓存以测试加载逻辑:
js
localStorage.removeItem('i18nLblsLang');
localStorage.removeItem('__i18n_version');
热更新支持
当前方案天然支持热更新:
- 服务端更新语言文本
- 客户端下次访问时因版本号变化自动重新加载
- 无需重新构建前端
Section sources