字段级本地化将多语言值存为嵌套对象(如{title:{en:"Hello",zh:"你好"}}),适合字段少、语言固定场景;文档级本地化则为每种语言建独立文档,通过refId和locale关联,适合语言动态增减或内容差异大场景。

字段级本地化:用嵌套对象存不同语言的值
直接在同一个文档里为每个多语言字段建一个对象,比如 title 变成 { title: { en: "Hello", zh: "你好", ja: "こんにちは" } }。适合字段数量少、语言固定、查询常按当前语言过滤的场景。
常见错误是把语言代码写成大写(EN)或带下划线(zh_CN),MongoDB 本身不校验,但应用层解析容易出错;建议统一用小写 ISO 639-1 码(en、zh、ja)。
- 查询当前语言时,用点号路径:
db.posts.find({ "title.en": { $exists: true } }) - 更新单语言字段要避免覆盖整个对象:别用
$set: { "title": { "en": "New" } },而用$set: { "title.en": "New" } - 索引需显式为常用语言字段建立,例如
db.posts.createIndex({ "title.en": 1 }),否则.find({ "title.en": ... })不走索引
文档级本地化:每种语言单独存一条文档
把同一内容的不同语言版本拆成多个文档,靠 locale 字段和 refId 关联,比如英文文档 { refId: "post_123", locale: "en", title: "Hello", content: "..." },中文文档 { refId: "post_123", locale: "zh", title: "你好", content: "..." }。
这种结构适合语言动态增减、需要独立权限控制、或内容差异大(比如营销文案需完全重写)的场景。但会显著增加文档数量和查询复杂度。
- 必须为
refId和locale建联合索引:db.translations.createIndex({ refId: 1, locale: 1 }, { unique: true }),防止重复插入 - 查“某篇文章所有语言版本”时用
refId查询,但查“某语言下所有文章”就得依赖locale索引,否则慢 - 聚合时容易漏掉缺失语言:用
$lookup关联主文档和翻译文档,记得加as后用$unwind+preserveNulls: true处理无对应语言的情况
用 $switch 或应用层做语言回退
MongoDB 没有内置语言 fallback 机制,比如用户请求 zh-Hans 但只有 zh,得自己处理。要么在聚合阶段用 $switch 判断优先级,要么把回退逻辑放在应用层——后者更灵活也更常见。
硬在聚合里写多层 $switch 容易让 pipeline 膨胀且难维护,尤其当支持 zh-Hans → zh → en 这类链式回退时。
- 推荐做法:应用层根据用户
Accept-Language解析出语言列表(如["zh-Hans", "zh", "en"]),然后发起一次查询,用$in查locale字段:{ locale: { $in: ["zh-Hans", "zh", "en"] } },取第一个命中的结果 - 如果必须用聚合,
$switch的branches顺序就是回退顺序,但注意default分支不能省,否则没匹配到就返回 null - 字段级方案中,回退逻辑只能靠应用层读取整个
title对象后判断,无法用索引加速
不要忽略 _id 和引用一致性
无论选哪种策略,_id 类型和跨文档引用方式直接影响扩展性和数据完整性。用字符串 ID(如 UUID 或业务 ID)比 ObjectId 更利于多语言文档对齐和调试。
字段级方案看似简单,但一旦要加新语言,就得批量更新所有文档;文档级方案改起来自由,但 refId 必须和源文档 _id 类型一致,否则关联失效。
- 避免用 ObjectId 当
refId:它不可读、难调试,且不同语言文档之间无法直观比对 - 如果源文档
_id是字符串(如"post_abc"),翻译文档的refId也必须是同值字符串,不能是ObjectId("...") - 删除原文档时,字段级方案自动清理,文档级方案必须手动删所有对应翻译文档,否则留脏数据
explain() 看查询是否真走了索引,才真正算稳了。


















