ADR-193: A draft element does not turn a connected page into a mixed one
- Status
-
Accepted
- Date
-
2026-09-21
- Amends
-
ADR-146 (its free-mode element, on a page that translates in connected mode)
- Authors
-
Netresearch DTT GmbH
Context
create_ (ADR-146) writes the element
in the language it is asked for and never sets a translation parent. ADR-146
recorded that as deliberate and named the cost among its consequences: the
description points a model at create_, and "a model that
ignores both produces an element an editor has to clean up".
On the Netresearch demo installation a model did. It created elements in language 1 on a page that also holds connected translations in language 1, and the page module reports Inconsistent content detected in language "…" on that page.
That warning has an exact condition. It was read in
TYPO3CMSBackend
of typo3/ 14.3.7, the version this repository resolved on the
day of this record:
- The default language is never judged; the method returns early for it.
- For any other language it walks the
tt_rows of that language on the page. Deleted rows are out, hidden rows are in, the workspace restriction of the acting user applies, and rows for "all languages" (content sys_) are skipped.language_ uid = - 1 - A row with
l18n_marks the language as holding standalone content, a row withparent = 0 l18n_as holding translations. When both marks are set the mode isparent > 0 mixedand the warning is queued. - The warning is suppressed when
Drawingis true, whichConfiguration:: get Allow Inconsistent Language Handling () Drawingreads from the page TSconfigConfiguration:: create () mod.with a plain cast to bool.web_ layout. allow Inconsistent Language Handling
The tool only ever adds rows of the first kind. So one fact decides whether a call produces the warning: whether the page already holds a row of the second kind in that language.
Decision
create_ refuses a call with language greater than
zero when the target page already holds at least one tt_ row with
that sys_ and l18n_. Deleted rows do not count,
hidden rows do — as core counts them, and every draft this extension writes is
hidden.
The refusal is resolved in the tool's plan, so it is reached before
anything is written, and preview and the viewer gate answer with
it as they do for every other refusal of this tool. It comes after the
neutral Page not found or not permitted., because it names the page: a user
who may not edit the page learns nothing about what is on it.
The message names the page uid and the language uid and says what to do
instead: create the element in the default language (0), translate it with
create_ or the translation tools of the CMS, and tell the
editor that the translation is a separate step. The tool description and the
language argument description say the same on the wire, so a model can
choose before it calls.
What stays allowed, and why
The default language. Unchanged; it is never queried, as core never judges it.
Free mode. A page that holds no row in the language, or only standalone rows, takes another standalone one. A page that is free-mode throughout is consistent, core raises nothing on it, and ADR-146's reasoning for creating without a translation parent still holds there.
The integrator's opt-in. When the page TSconfig sets
mod., the tool does not refuse.
It is the switch that silences the same warning in core, read the same way
(rootline-merged through Backend, cast to
bool), so the same switch lifts the refusal. The row check itself is wider
than core's, as the two sections below state. The TSconfig
is only read once a connected row was found.
Workspaces
No workspace code is added. Every builtin writer refuses outside the live
workspace (Writes), so the live workspace is the
only one a write happens in.
The query carries the deleted restriction and no workspace restriction, which is the one place it is wider than core's: a connected translation that exists only as another workspace's draft counts. That is deliberate. The draft sits on the page and becomes a live connected translation when that workspace is published, at which point a free element created in between would mix the page.
What this does not prevent
The opposite order. A page that holds only free elements in a language becomes
mixed the moment a connected translation is added to it — through
create_, through the backend's Translate wizard, or
through an extension that translates automatically. This record guards one
writer against one direction; it does not make mixed pages impossible, and it
does not repair a page that already is one.
Nor does it judge the page as a whole. A page that is already mixed is refused like a connected one, because it holds a connected row; nothing is reported about rows the tool did not write.
Core's query is open to listeners of
Modify; the tool's is not. An installation
whose listener hides rows from the page module can therefore be refused for a
connected row its editors do not see there.
Consequences
✓ This tool can no longer be the cause of the page module's inconsistency warning on a page that translates in connected mode.
✓ The refusal names the way that works — the default language, then a translation — instead of only declining.
✕ One more query per call with a non-default language, and a TSconfig resolution when that query finds something.
✕ An editor who wants a standalone element beside connected translations, and whose integrator has not set the TSconfig switch, cannot get it from this tool. The backend still lets them create it by hand.
✕ The opposite order stays open, as stated above.
Revisit when
create_ or another writer is found to produce mixed pages
from the other direction in practice, or core changes the condition
Content applies — the guard mirrors that
method and has to follow it.