先说结论
多语言 CMS 的表结构,难点不在"字段多",在"翻译放哪"。放错地方,后面每一个查询都要打补丁。
五组表
| 组 | 表 | 职责 |
|---|---|---|
| 内容 | products / cases / solutions / news / pages | 五类内容,结构相近 |
| 组织 | categories / tags / relations | 分类树、标签、关联 |
| 语言 | languages / translations | 语种开关、译文 |
| 配置 | configs / settings / menus | 站点配置、导航 |
| 系统 | users / audit_logs / tokens | 账号、审计、API |
翻译放哪:两种方案
方案 A:同表加字段(title_en / title_ja …)
- 优点:一次查询拿全
- 缺点:33 个语种就是 33 组字段,表结构爆炸,加语种要改表
方案 B:独立译文表(content_id + lang + field + value)
- 优点:加语种不改表,翻译状态好管
- 缺点:查询要 join 或二次聚合
我选 B 的变体:译文按语种存 JSON 字段(translations 存 {"ja": {...}, "ru": {...}}),兼顾灵活与查询效率。
关键设计:翻译状态与版本
每条内容带三个字段:
translation_status:pending / translating / done / failedtranslation_version:v1:<sha1>,原文的哈希translated_at:最后回写时间
改稿时版本号变化,旧译文自动作废——这是整个系统最重要的一个设计。
索引怎么建
(category_id, status, published_at)列表页(slug)详情页(唯一)(translation_status)翻译队列扫描(lang, status)语言维度查询
迁移策略
首发用一份 baseline 迁移建全表,后续每个版本一个增量迁移。好处是:新客户装完就是最新结构,老客户跑增量即可。
踩过两个坑:一是把 33 个语种塞进一个 JSON 字段,列表页整列读出,内存直接翻倍——后来改成列表只取当前语种;二是分类树用递归查询,层级一深就 N+1,改成物化路径(path 字段)后一次查询搞定。