data-i18n键名不应嵌套,必须采用扁平结构(如"header_title"),否则js查找需手动解析字符串、易出错且无法静态校验;所有语言文件键名须完全一致,缺失时保留空字符串或占位符,避免显示原始键名或空白。

data-i18n 键名要不要嵌套?别嵌
键名嵌套(如 {"ui": {"header": {"title": "欢迎"}}})是常见误区。JS 查找时不会自动展开层级,t('ui.header.title') 或遍历 data-i18n="ui.header.title" 都得手动解析字符串,容易出错、难调试、无法静态校验。
真正可行的是扁平结构:{"header_title": "欢迎", "form_email_required": "邮箱不能为空"}。所有语言文件键名必须完全一致,哪怕某语言暂未翻译,也保留空字符串或占位符,否则查不到 key 就留白——用户看到的是原始键名或空白。
- 键名用下划线或点分隔均可,但必须统一;推荐下划线(
nav_home),避免 JS 中误当对象链访问 - 不要靠 class/id 推断键名,
data-i18n必须显式写死,JS 只认这个属性 - 嵌套结构在构建时可能被 loader 处理,但纯 HTML + JS 方案里毫无意义,反而增加 fallback 逻辑复杂度
怎么让 t('common.error.network') 这种调用生效?
这种带点的键名本身没问题,但前提是你的翻译函数 t() 明确支持路径解析,且语言包是嵌套对象 —— 这和前面说的「扁平 JSON 文件」冲突。二者不可混用。
如果你坚持用点号语法,就必须:(1)加载时把扁平 JSON 转成嵌套对象;(2)t() 内部做 key.split('.').reduce((o, k) => o?.[k], dict);(3)所有语言包必须严格保持相同嵌套深度,否则 t('a.b.c') 在某个语言里缺 a.b 就返回 undefined。
- 更稳妥的做法:放弃点号,用扁平键名 + 命名空间前缀,比如
common_error_network - 若已有旧系统用点号,务必在 fetch 后统一 normalize:把嵌套结构 flatten 成一层,再交给 DOM 替换逻辑
- 注意 IE11 不支持
dataset驼峰访问,el.dataset.i18n会失效,得用el.getAttribute('data-i18n')
动态拼接键名(如 t('btn.' + type))安全吗?
不安全,除非你 100% 控制 type 的取值范围。用户输入、URL 参数、后端返回值都可能注入非法键名,导致查不到翻译、留白,甚至触发 fallback 到默认语言时暴露内部键名。
正确做法是预定义白名单映射:
const ACTION_LABELS = {
'save': '保存',
'delete': '删除',
'cancel': '取消'
};
t('btn_' + (ACTION_LABELS[type] ? type : 'default'));
- 永远不要直接拼接用户可控字段进键名
- 服务端返回的枚举值(如 status=“pending”)也要先映射,不能
t('status.' + res.status) - 插值(
"{name} 已提交")可以,但插值内容不参与键名生成
多级查找失败时,fallback 到哪一级?
不是“查不到就往上找父级”,而是按明确策略降级:先试完整 BCP 47 码(zh-HK),再截主语言(zh),最后退到硬编码默认对象(如内置英文)。没有“自动向上继承”这回事。
- fetch
./locales/zh-HK.json失败 → 重试./locales/zh.json→ 再失败 → 用内置{ header_title: "Welcome" } - 不要 fallback 到浏览器
navigator.language,它可能为空或错误(尤其 WebView) - localStorage 存的
preferred-lang只用于记忆用户选择,首次加载仍应以 URL 参数 > localStorage > navigator.language 为序
多级字典真正的复杂点不在结构,在 fallback 时机和边界控制:JSON 加载失败、key 缺失、lang 属性不同步、动态节点漏处理——这些地方一漏,用户看到的就是 data-i18n="xxx" 原样显示。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











