Connected translation without l10nSource never shows source changes: stamp the missing hash once on open #343

Closed
opened 2026-10-23 08:42:53 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

A connected translation whose elements (or front matter) carry no l10nSource is never marked "changed in the default language", however much its source changes afterwards. Opening such a translation in the editor should stamp the missing hashes once — treating the element as translated as of that moment — so that later changes to the source are indicated like everywhere else.

Current behavior

Reported on the hosted editor (site cloonar): the user edits an existing, enabled element of a page in the default language; the default-language tree shows the page's "modified" badge, but the German tree keeps the page's green "translated" dot and the Languages view shows no change. The German translation is connected (l10n: connected) and, as confirmed on the site, its element shortcodes have no l10nSource param.

The engine's rule: l10n.ChangedUIDs lists only elements whose stored l10nSource differs from the source element's current hash; "an element without l10nSource is unknown and never listed". markOutdated (the tree) and translationInfo (the page answer's changed list, which drives the element cards' "changed" marks) both build on it, and pageops.TranslationPageChanged does the same for the page-level l10nSource. Nothing stamps a missing hash: l10n.StampSource runs only when an element is translated or saved in the translation, and opening the page (openPage, which already repairs the connected invariant and persists the repair) leaves the param absent. Reproduced locally: strip l10nSource from a translated element, change its source body in the default language, and GET /api/tree?lang=de stays translated; opening the translation does not add the param.

Translations end up unstamped when they were written by hand (AGENTS.md "Translations by hand" says to leave the param out), created by harness lang add (the home page) or any path that is not the editor's translate/save, or imported from an existing site (migrate-site).

Desired behavior

Opening a connected translation in the editor (GET /api/page and GET /api/page/elements in a non-default language — the same reads that repair the invariant) stamps every translated element, item and the front matter that has no l10nSource with the current hash of its source, and persists that like the repair. From then on a change to the source is reported exactly as for an element translated in the editor: the page answer's changed list, the element cards' marks, the tree's "outdated" state and the Languages view.

Edge cases:

  • An element whose source the page lacks (no same-uid, same-name element) gets no stamp — the repair removes it anyway.
  • A free translation (l10n not connected) is left alone: it carries no hashes by design.
  • An element that already has an l10nSource keeps it, even if stale — stamping must never overwrite an existing hash, or it would erase a real "changed" verdict.
  • Before the translation is opened once, the tree keeps saying "translated": the tree is a read and does not write. This is accepted; the user sees the right state from the first open on.
  • The snapshot/undo entry for the write is the existing repair translation one (a stamp is part of holding the translation to the invariant); no new snapshot kind.

Decisions

  • Stamp once with the current hash rather than treating a missing hash as "changed" — the user's call: an unstamped translation is one translated offline; what matters is that future changes show. Treating it as changed would mark every hand-written translation "changed" until each element is re-saved, which is noise.
  • Stamp on open, not in the tree read — the tree is polled; a GET must not write. The open already persists a repair, so it is the one place that may.
  • harness check is not changed to warn on a missing hash — after this change a missing hash is transient and harmless.

Key interfaces

  • l10n.StampSource(source, translated, schemas) stamps one translated element and its items; a new or extended owner is needed for "stamp every element of a translation that lacks a hash, and the page-level hash" — it must reuse Hash/ItemHash/PageHash, never re-derive them. The rule "is this element stamped" is sourceParam in internal/l10n; keep it the one owner.
  • The open path in internal/server (openPage, which calls l10n.Repair and persists) — the natural caller.
  • l10n.PageHash / l10n.StampPageChanges for the front matter's l10nSource.
  • scaffold/site/AGENTS.md "Translations" → "Change tracking" ("A missing l10nSource means unknown and shows no mark") and "Translations by hand" ("Never write l10nSource… no changed mark until the element… is next saved") must say instead that the editor stamps a missing hash when the translation is first opened. docs/api.md GET /api/page "Languages"/"Writes" describe what opening a connected translation repairs and persists; add the stamp there. docs/architecture.md "Multilingual sites" mentions the repair in one sentence; adjust if it states what the open writes. The edit-content and migrate-site skills point at those AGENTS.md sections and restate nothing, so they only need a re-read for a sentence that contradicts the new behaviour.

Acceptance criteria

  • Opening a connected translation whose element lacks l10nSource writes the current source hash onto that element (nested elements and items included), with the file otherwise unchanged and canonical.
  • A connected translation whose front matter lacks l10nSource gets the page-level hash on open.
  • An existing l10nSource is never overwritten by the open.
  • A free translation is not stamped.
  • After the stamp, changing the source element (body or a General-tab param) in the default language makes the page answer list the uid in changed and the tree report outdated; changing the source page's title makes pageChanged true.
  • The regression from the report: a translated element without l10nSource, source changed afterwards, page opened once → tree outdated (fails before the change, passes after).
  • AGENTS.md and docs/api.md describe the stamp; the drift tests in internal/element, scaffold, docs and internal/server pass.

How to verify

  • go test ./internal/l10n/ ./internal/server/ ./scaffold/ ./docs/ ./internal/element/.
  • Manual: harness init a site, harness lang add de, translate a page connected, delete the l10nSource params from the .de.md by hand, open the German page in the editor, change the English element, and watch the German tree row turn "outdated".

Assumptions to check

  • That openPage is the only read that persists a repair (docs/api.md lists GET /api/page and GET /api/page/elements) — stamp on every such read, not only one.
  • That the hosted site's unstamped translations are otherwise sound (uids present); if uids are missing too, the repair removes those elements first, which is existing behaviour.

Out of scope

  • Marking a translation "outdated" because the source gained a new element the translation lacks (UntranslatedUIDs); today only the page answer's untranslated list shows it. A separate decision.
  • Syncing or hashing an element's enabled/disabled state (per language by design).
  • Stamping from harness check or harness migrate.
  • The preview cache issue, filed separately.

Blocked by

None - can start immediately

> *This was generated by AI during triage.* ## Summary A connected translation whose elements (or front matter) carry no `l10nSource` is never marked "changed in the default language", however much its source changes afterwards. Opening such a translation in the editor should stamp the missing hashes once — treating the element as translated as of that moment — so that later changes to the source are indicated like everywhere else. ## Current behavior Reported on the hosted editor (site `cloonar`): the user edits an existing, enabled element of a page in the default language; the default-language tree shows the page's "modified" badge, but the German tree keeps the page's green "translated" dot and the Languages view shows no change. The German translation is connected (`l10n: connected`) and, as confirmed on the site, its element shortcodes have no `l10nSource` param. The engine's rule: `l10n.ChangedUIDs` lists only elements whose stored `l10nSource` differs from the source element's current hash; "an element without l10nSource is unknown and never listed". `markOutdated` (the tree) and `translationInfo` (the page answer's `changed` list, which drives the element cards' "changed" marks) both build on it, and `pageops.TranslationPageChanged` does the same for the page-level `l10nSource`. Nothing stamps a missing hash: `l10n.StampSource` runs only when an element is translated or saved in the translation, and opening the page (`openPage`, which already repairs the connected invariant and persists the repair) leaves the param absent. Reproduced locally: strip `l10nSource` from a translated element, change its source body in the default language, and `GET /api/tree?lang=de` stays `translated`; opening the translation does not add the param. Translations end up unstamped when they were written by hand (AGENTS.md "Translations by hand" says to leave the param out), created by `harness lang add` (the home page) or any path that is not the editor's translate/save, or imported from an existing site (`migrate-site`). ## Desired behavior Opening a connected translation in the editor (GET /api/page and GET /api/page/elements in a non-default language — the same reads that repair the invariant) stamps every translated element, item and the front matter that has no `l10nSource` with the current hash of its source, and persists that like the repair. From then on a change to the source is reported exactly as for an element translated in the editor: the page answer's `changed` list, the element cards' marks, the tree's "outdated" state and the Languages view. Edge cases: - An element whose source the page lacks (no same-uid, same-name element) gets no stamp — the repair removes it anyway. - A free translation (`l10n` not `connected`) is left alone: it carries no hashes by design. - An element that already has an `l10nSource` keeps it, even if stale — stamping must never overwrite an existing hash, or it would erase a real "changed" verdict. - Before the translation is opened once, the tree keeps saying "translated": the tree is a read and does not write. This is accepted; the user sees the right state from the first open on. - The snapshot/undo entry for the write is the existing `repair translation` one (a stamp is part of holding the translation to the invariant); no new snapshot kind. ## Decisions - Stamp once with the current hash rather than treating a missing hash as "changed" — the user's call: an unstamped translation is one translated offline; what matters is that *future* changes show. Treating it as changed would mark every hand-written translation "changed" until each element is re-saved, which is noise. - Stamp on open, not in the tree read — the tree is polled; a GET must not write. The open already persists a repair, so it is the one place that may. - `harness check` is not changed to warn on a missing hash — after this change a missing hash is transient and harmless. ## Key interfaces - `l10n.StampSource(source, translated, schemas)` stamps one translated element and its items; a new or extended owner is needed for "stamp every element of a translation that lacks a hash, and the page-level hash" — it must reuse `Hash`/`ItemHash`/`PageHash`, never re-derive them. The rule "is this element stamped" is `sourceParam` in `internal/l10n`; keep it the one owner. - The open path in `internal/server` (`openPage`, which calls `l10n.Repair` and persists) — the natural caller. - `l10n.PageHash` / `l10n.StampPageChanges` for the front matter's `l10nSource`. - `scaffold/site/AGENTS.md` "Translations" → "Change tracking" ("A missing `l10nSource` means unknown and shows no mark") and "Translations by hand" ("Never write `l10nSource`… no changed mark until the element… is next saved") must say instead that the editor stamps a missing hash when the translation is first opened. `docs/api.md` GET /api/page "Languages"/"Writes" describe what opening a connected translation repairs and persists; add the stamp there. `docs/architecture.md` "Multilingual sites" mentions the repair in one sentence; adjust if it states what the open writes. The `edit-content` and `migrate-site` skills point at those AGENTS.md sections and restate nothing, so they only need a re-read for a sentence that contradicts the new behaviour. ## Acceptance criteria - [ ] Opening a connected translation whose element lacks `l10nSource` writes the current source hash onto that element (nested elements and items included), with the file otherwise unchanged and canonical. - [ ] A connected translation whose front matter lacks `l10nSource` gets the page-level hash on open. - [ ] An existing `l10nSource` is never overwritten by the open. - [ ] A free translation is not stamped. - [ ] After the stamp, changing the source element (body or a General-tab param) in the default language makes the page answer list the uid in `changed` and the tree report `outdated`; changing the source page's title makes `pageChanged` true. - [ ] The regression from the report: a translated element without `l10nSource`, source changed afterwards, page opened once → tree `outdated` (fails before the change, passes after). - [ ] AGENTS.md and docs/api.md describe the stamp; the drift tests in `internal/element`, `scaffold`, `docs` and `internal/server` pass. ## How to verify - `go test ./internal/l10n/ ./internal/server/ ./scaffold/ ./docs/ ./internal/element/`. - Manual: `harness init` a site, `harness lang add de`, translate a page connected, delete the `l10nSource` params from the `.de.md` by hand, open the German page in the editor, change the English element, and watch the German tree row turn "outdated". ## Assumptions to check - That `openPage` is the only read that persists a repair (docs/api.md lists GET /api/page and GET /api/page/elements) — stamp on every such read, not only one. - That the hosted site's unstamped translations are otherwise sound (uids present); if uids are missing too, the repair removes those elements first, which is existing behaviour. ## Out of scope - Marking a translation "outdated" because the source gained a *new* element the translation lacks (`UntranslatedUIDs`); today only the page answer's `untranslated` list shows it. A separate decision. - Syncing or hashing an element's enabled/disabled state (per language by design). - Stamping from `harness check` or `harness migrate`. - The preview cache issue, filed separately. ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#343
No description provided.