spatie/laravel-translatable 核心在于明确翻译存储结构、读写机制与事务一致性:翻译存于独立表(如 product_translations),需含 model_id、locale、attribute、value 四字段及唯一索引;读取按 locale→fallback→default 顺序匹配,不合并字段;更新必须用 settranslation() + save() 保证事务安全;关联翻译需显式 withtranslation() 预加载。

用 spatie/laravel-translatable 管理模型翻译字段,核心不是“加个 trait 就完事”,而是要明确:翻译数据存在哪、怎么读、怎么写、事务里会不会丢——否则上线后查不到德语内容、保存时漏掉法语、并发更新时覆盖翻译,都是高频事故。
翻译数据默认存在独立表里,不是 JSON 字段
包默认采用「主表 + 翻译表」双表结构,比如 products 对应 product_translations。翻译表必须有 model_id、locale、attribute、value 四个关键字段,且 (model_id, locale, attribute) 应设为唯一索引——否则 upsert 或 save() 时可能静默失败或重复插入。
常见错误现象:
- 调用
$product->setTranslation('name', 'de', 'Tisch')->save()后查数据库发现没写入:检查product_translations表是否真有这四列,以及是否建了唯一索引 - 手动在翻译表里插了一条记录,但
$product->translate('de')->name返回空:确认locale值是'de'(不是'de_DE'),且attribute是小写字符串'name'
读取翻译时,locale 和 fallback 必须对得上配置
translate($locale) 不是简单查表,它会按顺序尝试:当前 locale → 配置的 locale_fallbacks → 默认 locale(app()->getLocale())。如果中间某一级查到空值,就继续往下找;但只要某一级查到非空,就立刻返回,不会合并。
所以别指望 translate('fr') 自动补上 en 里有的字段而 fr 里没有的字段——它只返回 fr 行里 value 不为空的那些属性。
实操建议:
- 在
config/translatable.php中显式定义'locale_fallbacks' => ['en' => ['en', 'zh']],避免依赖隐式 fallback - 需要“有就用,没有就用默认语言”的语义,用
translateOrDefault('fr'),它会在fr找不到时自动 fallback 到默认 locale 的完整翻译对象 - 调试时直接 dump
$product->translations,看 Eloquent 加载进来的到底是哪些 locale 的记录
事务中更新翻译字段,必须走 setTranslation + save,不能单独 save 翻译模型
这是最常踩的坑:在 DB::transaction 里写 $product->translate('es')->name = 'Mesa'; $product->translate('es')->save();,结果事务回滚后只有主表回滚,翻译表已提交。
原因:$product->translate('es') 返回的是一个独立的 Eloquent 模型实例,默认用自己的连接执行 save(),不绑定父事务。
正确做法只有一条路:
- 所有修改必须通过
setTranslation('field', 'locale', 'value')写入内存,最后统一调$product->save() - 确保数据库引擎是 InnoDB,且连接配置
'strict' => true,否则事务隔离级别不足会导致部分写入成功 - 避免在事务中触发模型事件(如
saving),事件回调里再调translate()->save()同样会脱钩
关联模型的翻译不会自动预加载,with() 里要显式指定
主模型用了 HasTranslations,关联模型也用了,不代表 with('categories') 会自动带出 categories 的德语名。Eloquent 不知道你要哪门语言,更不会帮你调 translate('de')。
解决方式取决于你用的包版本和场景:
- 用
spatie/laravel-translatable:在关系定义里加->withTranslation('de'),例如Category::with(['products' => function ($q) { $q->withTranslation('de'); }]) - 用
astrotomic/laravel-translatable:改用withTranslation()方法,语法一致 - 如果关联太多、语言动态切换,推荐在控制器里统一做一次
load('relationName:*,name,description')再批量translate(),比嵌套 with 更可控
真正麻烦的不是语法,而是翻译字段缺失时的静默行为——它不会报错,只是返回空字符串。上线前务必用非默认 locale 实测所有列表页和详情页。











