decaporg / decaporg/decap-cms

preSave handler can't write entry.i18n[locale].data anymore for multiple_folders i18n collections -- regression from #7667

Open
#7,966 0 comments 0 reactions 0 assignees View on GitHub
area: i18n
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.