Skip to content

fix(translator): translate without modifying the caller's source document - #135

Merged
SearheiParkhamchuk merged 1 commit into
mainfrom
fix/translator-detach-leaf-values
Sep 11, 2026
Merged

SearheiParkhamchuk merged 1 commit into
mainfrom
fix/translator-detach-leaf-values

Conversation

@SearheiParkhamchuk

Copy link
Copy Markdown
Contributor

The defect

FieldChunkCollector seats the caller's own leaf value into the pipeline's working tree, and write-back then mutates it in place.

For a text field that is harmless: strings are immutable, and the write lands on the rebuilt container's key. For a richText field the value is a whole tree of objects, and the pipeline holds references straight into it — so translating a document rewrote the text nodes of the source document the caller passed in.

The outer tree was never the problem: DataReconciler already rebuilds every container level. The corruption is entirely inside a rich-text leaf, which the field walker treats as one value and hands over by reference.

How it showed up

Through provenance. captureFingerprint had to run before the pipeline, or it hashed the translation instead of the source and reported every fresh translation as instantly stale.

Nothing enforced that. The ordering rested on two statements being in the right order in translate-document/handler.ts, and PipelineContext.sourceData being readonly forbids reassigning the reference, not writing sourceData.body.root.children[0].text.

The test that guarded it could not have caught a regression either: it emulated the mutation by overwriting a top-level string key, which the pipeline never does.

Reproduced on main, in the new test — two different hashes for the same source document, before and after one translation:

expected '3d3f7ab6…' to be 'fc12e17c…'

The fix

dataRef[key] = isObject(sourceValue) ? structuredClone(sourceValue) : sourceValue;

Only the leaves the collector selects are copied — those are exactly the ones a write can reach, since the branch runs under isTranslatableLeaf(field) && strategy.shouldTranslate(...). Scalars pass through untouched, and structuredClone is never handed an arbitrary document value.

Cloning at DataReconciler's leaf instead was considered and rejected: it returns every leaf, translatable or not, so it would put values through structuredClone that nothing writes and that it may not be able to copy.

The prohibition on structuredClone recorded in projectFieldLike.ts does not apply here — it is about field definitions, which embed Lexical editor configs containing async functions. Document data arrives from payload.findByID at depth: 0.

Consequences

  • ProvenanceService.captureFingerprint no longer demands pristine input. Its docblock now states what is true and records why the ordering used to matter.
  • The handler's statement order is left as it is — it is harmless either way now, and reordering working code buys nothing.
  • The handler test keeps its hostile stub and says plainly that it guards against a regression rather than mirroring what the pipeline does.

Verification

  • 1363 unit tests, 4 new. Each "source unchanged" assertion is paired with a "something was translated" assertion, so a pipeline that did nothing cannot pass.
  • One of the four compares computeSourceFingerprint before and after a real translateContent run — the check that proves the ordering rule is retired, run rather than inferred.
  • check-types clean. Lint 58 warnings, 0 errors — measured on a clean checkout of main and again with the change: identical.
  • turbo run build passes, including the tsc declaration build.
  • Mutation-checked: removing the copy turns exactly the three new rich-text assertions red.

Scope

Deliberately narrow. Write-back still mutates the nodes it now owns; replacing that with a rebuild is a larger change with a competing design already on record, and is not attempted here.

…ment

`FieldChunkCollector` seated the caller's own leaf value into the pipeline's
working tree, and write-back then mutated it in place. For a string that is
harmless — strings are immutable, and the write lands on the rebuilt
container's key. For a richText field the value is a whole tree of objects,
so translating a document rewrote the text nodes of the source document the
caller had passed in.

The visible consequence was provenance. `captureFingerprint` had to be called
before the pipeline ran, or it hashed the translation instead of the source
and reported every fresh translation as instantly stale. Nothing enforced
that: the ordering rested on two statements being in the right order in
`translate-document/handler.ts`, and the test guarding it emulated the
mutation on a top-level string, which the pipeline never touches — so it
pinned the order without being able to catch the regression it named.

Object-valued leaves are now detached when they are seated. Only the leaves
the collector selects are copied — the ones a write can reach — so scalars
and untranslated values pass through untouched, and `structuredClone` is
never handed an arbitrary document value.

`ProvenanceService.captureFingerprint` no longer demands pristine input; its
docblock records why the ordering used to matter. The handler test keeps its
hostile stub and now says plainly that it guards against a regression rather
than mirroring what the pipeline does.

Verification: 1363 unit tests (4 new), including a before/after fingerprint
comparison across a real translateContent run; check-types clean; lint 58
warnings and 0 errors, identical to main for the same files; the declaration
build passes. Removing the copy turns the three new richText assertions red.
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ideal-cms Ready Ready Preview Sep 10, 2026 9:43pm UTC

Request Review

@SearheiParkhamchuk SearheiParkhamchuk self-assigned this Sep 11, 2026
@SearheiParkhamchuk
SearheiParkhamchuk merged commit 4653f27 into main Sep 11, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.11.5 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant