preSave handler can't write entry.i18n[locale].data anymore for multiple_folders i18n collections -- regression from #7667
- Dominant language
- JavaScript
- Stars
- 19.4k
- Forks
- 3.1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 9
Description
I think I've hit a real regression here and want to flag it before someone else burns a few hours chasing it like I just did.
I'm on `decap-cms@3.14.1` (bundles `decap-cms-core@3.16.0`), GitHub backend, `editorial_workflow: true`, and a collection with `i18n: true` using the `multiple_folders` structure (each locale lives in its own top-level folder, e.g. `content/kb-articles/en/slug.md`, `content/kb-articles/pt-br/slug.md`). I have a `preSave` handler that writes AI-translated content into another locale, using exactly the pattern described in the fix for #4729:
```js
CMS.registerEventListener({
name: 'preSave',
handler: async ({ entry }) => {
return entry.setIn(['i18n', 'pt-br', 'data', 'title'], translatedTitle);
// ...a handful more setIn calls for other fields
},
});
```
That's the exact pattern from the #7227 PR description. On my version this does not write to the pt-br file at all. Instead:
- the pt-br file is left completely untouched (whatever was already there before my handler ran)
- the EN (default locale) file's frontmatter gets clobbered with what looks like the entire internal entry object -- `data`, `i18n`, `path`, `meta`, `raw`, `slug`, `mediaFiles`, `collection`, all of it, dumped straight into the file as YAML
Took me a while to track down but I'm fairly confident I found it. `invokeEvent` in `registry.js`:
```js
let _data = { ...data };
for (const { handler, options } of handlers) {
const result = await handler(_data, options);
if (result !== undefined) {
const entry = _data.entry.set('data', result);
_data = { ...data, entry };
}
}
return _data.entry;
```
Whatever the handler returns gets set as `entry.data` (not swapped in as the new entry), and then the *whole* entry -- now with my full returned entry nested one level inside its own `.data` -- gets returned. `persistEntry` in `backend.ts` sees `updatedEntity.get('data')` is defined and swaps the whole thing in as the new entry draft, and `entryToRaw` dumps `entry.get('data').toJS()` straight into the file. Since my handler's `setIn(['i18n', 'pt-br', ...])` only touched the top-level `i18n` branch and never the one that actually gets read afterwards (now one level deeper because of the extra nesting), the pt-br write never lands anywhere real.
I bisected it to #7667 (merged 2026-04-22). Before that, `invokeEvent` returned `_data.entry.get('data')`, which combined with the `.set('data', result)` line above made it a round trip back to exactly `result` -- i.e. the handler's own return value, unwrapped again -- so the #7227 pattern worked as documented. #7667 changed the final line to `return _data.entry`, which broke that round trip for anyone using the i18n `setIn` pattern, even though it was fixing a real and completely separate bug for file collections. I checked `main` just now (2026-09-01) and it's still there.
I confirmed `decap-cms@3.12.2` (published 2026-04-17, just before #7667 landed) still has the old, working `return _data.entry.get('data');` line, so I'm pinning to that as a stopgap for now.
Not sure what the right fix looks like without reintroducing the bug #7667 was fixing -- maybe `persistEntry` needs to detect and unwrap this specific shape, or `invokeEvent` needs to treat i18n entries differently. Happy to put together a minimal repro repo if that's useful, and I can take a swing at a PR if someone more familiar with this code can point me at which behavior is actually meant to be correct here.
**Versions:** `decap-cms@3.14.1` (core `3.16.0`), GitHub backend, `editorial_workflow: true`, i18n structure `multiple_folders`.
-- Marc
Contributor guide
Research direction
Start in registry.js at invokeEvent and then trace persistEntry and entryToRaw in backend.ts. Compare the current return behavior with the pre-#7667 behavior and the multiple_folders i18n path described here. Done means preSave writes the returned locale data to the intended file without regressing the file-collection behavior addressed by #7667.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100