focusreactive / focusreactive/payload-plugins
translator: skip_existing ignores staleness, so the admin indicator and the strategy disagree
- Dominant language
- TypeScript
- Stars
- 19
- Forks
- 0
- Avg merge
- 16h 54m
- Merged PRs (30d)
- 19
Description
## Summary in one line
The admin shows "this translation is out of date", and `strategy: "skip_existing"` then refuses to re-translate that very field — the two features answer the same question differently.
## What happens
1. Translate `de`. Provenance stores a fingerprint of the source's translatable content.
2. Edit the English source.
3. The admin's staleness indicator (#50) lights up: the translation is out of date.
4. Run a translation with `strategy: "skip_existing"`, which is the natural choice for "refresh what needs refreshing".
Nothing is re-translated. The endpoint returns `200`.
## Why
`skip_existing` has exactly one criterion — is the target field empty:
```ts
// src/core/translation-pipeline/strategies/SkipExisting.strategy.ts:11-14
shouldTranslate(ctx: StrategyContext): boolean {
if (isEmpty(ctx.sourceValue)) return false;
return this.isEmptyValue(ctx.targetValue);
}
```
Staleness is computed by `isRecordStale` (`src/core/domain/provenance/staleness.ts`), and its only consumers are the staleness HTTP routes that feed the admin indicator. The pipeline never asks.
So "already translated" means "not empty" — with no notion of *current*, and no notion of *reviewed*: a machine translation nobody read, a placeholder, or a half-typed word all count as done.
## Why the obvious fix is wrong
Making `SkipExistingStrategy` call `isRecordStale` looks like a few lines. It is a trap, because the two work at different granularities:
- **Staleness is per document-locale.** `computeSourceFingerprint(doc, schema)` is one sha256 over the whole document's translatable content, and `TranslationProvenanceRecord` stores one `sourceFingerprint` per `(collectionSlug, documentId, targetLocale)`.
- **The strategy decides per field.**
So the only thing that integration can express is a document-wide switch: if the document is stale, re-translate *every* non-empty field. That destroys work:
> A document has `title` and `body`. A reviewer corrects the German `title`. Somebody then edits the English `body`. The document is now stale, so the run re-translates the `title` too — overwriting a correction whose source never changed.
That is the same failure as #116, reached from a different direction and firing on every run rather than only on publish.
## What a correct fix needs
Per-field provenance, so the strategy can ask "did *this* leaf's source change":
1. compute a fingerprint per translatable leaf — cheap, the content projector already walks them;
2. store them — this changes the provenance collection's shape and needs a migration;
3. give `StrategyContext` a third input (the source value this leaf was translated from) — a contract change in `core`;
4. a strategy or mode that uses it, plus tests.
## A cheaper option worth considering instead
Leave `skip_existing` alone and add a separate, explicitly chosen strategy — "re-translate what is stale" — that re-translates everything when the document is stale. Coarse, and it has the same overwrite behaviour as above, but the editor picks it knowingly rather than being surprised by it. Much smaller, and it removes the contradiction between the indicator and the strategy without a storage migration.
## Notes
- #50 built the staleness detection; it was scoped to the indicator, and wiring it into translation was never in scope. This issue is about that gap, not a regression from it.
- Related but distinct: #116 is about publishing destroying a corrected draft.
- Found while reworking the draft/publish write path; that work does not change any of this.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.