Symmetrically translatable config-defined component trees, STEP 2: change config schema type for `inputs` to allow translating via core's Config Translation UI for ContentTemplates & PageRegions
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3582478. -->
Reported by: [wim leers](https://www.drupal.org/user/99777)
Related to !898 !901 !868
>>>
<h3 id="approach">Approach</h3>
<p>For high-level overview, see the <a href="https://www.drupal.org/node/3586291">change record</a>.</p>
<ol>
<li>ADR: !901
</li><li>Implementation MR #1: !962 in <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/canvas/issues/3586342" title="Status: Closed (fixed)">#3586342: Symmetrically translatable component trees, STEP 1: introduce `ComponentInstanceInputsConfigSchemaGeneratorInterface`</a></span>
</li><li>Implementation MR #2: !898, here
</li><li>Implementation MR #3: !831 in <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3582464" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3582464</a></span>
</li></ol>
<h3 id="overview">Overview</h3>
<p>The explicit inputs for a component instance in a config-defined component tree currently looks like this:</p>
<pre>diff --git a/config/schema/canvas.schema.yml b/config/schema/canvas.schema.yml<br>index 4c9d7514b..df8c05b86 100644<br>--- a/config/schema/canvas.schema.yml<br>+++ b/config/schema/canvas.schema.yml<br>@@ -360,7 +360,7 @@ canvas.component_tree_node:<br> constraints: { }<br> # @see \Drupal\canvas\Plugin\DataType\ComponentInputs<br> inputs:<br>- type: ignore<br>+ type: component_instance_inputs<br> label: 'Input values for each component in the component tree'<br> label:<br> # This is entirely optional; provides more context for content authors.</pre><p>That has been fine, because the <code>ValidComponentTreeItem</code> and <code>ComponentTreeMeetRequirements</code> validation constraints automatically turn the given config blobs into a <code>ComponentTreeItem</code> (the field type) and then use <em>its</em> validation constraints.</p>
<p>This is how Canvas is able to store component trees in both content and config entities with perfect consistency: because the same validation infrastructure is used for both.</p>
<h4>Config schema must become more precise to support translations</h4>
<p><strong>However, translating config-defined component trees</strong> means using Drupal core's <code>LanguageConfigOverride</code>, <code>config_translation</code> and related infrastructure (see <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3582464" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3582464</a></span>). All that infrastructure gets its information from … config schema.</p>
<p>⇒ config schema must become more precise.</p>
<p>💡 This <em>should</em> also make the "JSON-blobs-in-config-YAML" disappear, i.e.:</p>
<pre>component_tree:<br> - <br> uuid: fd141b6e-c57a-4355-9885-a8b2e60627d6<br> component_id: sdc.canvas_test_sdc.heading<br> component_version: 8c01a2bdb897a810<br> inputs: '{"text":{"sourceType":"entity-field","expression":"\u2139\ufe0e\u241centity:node:page\u241dtitle\u241e\u241fvalue"},"style":"primary","element":"h1"}'</pre><p>
would become</p>
<pre>component_tree:<br> -<br> uuid: fd141b6e-c57a-4355-9885-a8b2e60627d6<br> component_id: sdc.canvas_test_sdc.heading<br> component_version: 8c01a2bdb897a810<br> inputs:<br> text: {"sourceType":"entity-field","expression":"\u2139\ufe0e\u241centity:node:page\u241dtitle\u241e\u241fvalue"}<br> style: primary<br> element: h1</pre><p>
EDIT: </p>
<h3 id="proposed-resolution">Proposed resolution</h3>
<ol>
<li>✅ Ensure that the <code>ComponentInputsMapping extends Mapping</code> config schema type class gets its information from the same infrastructure as <code>ComponentTreeItem</code>'s <code>inputs</code> property: <code>public static ComponentInputs::resolveConfigSchemaMapping()</code> available that is used by both classes. That infrastructure was introduced by <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/canvas/issues/3586342" title="Status: Closed (fixed)">#3586342: Symmetrically translatable component trees, STEP 1: introduce `ComponentInstanceInputsConfigSchemaGeneratorInterface`</a></span> and is reused here.
</li><li>✅ Change from <code>type: ignore</code> to <code>type: mapping</code> with an explicitly specified class <code>ComponentInputsMapping</code> that subclasses core's <code>\Drupal\Core\Config\Schema\Mapping</code> to auto-generate at runtime the correct mapping definition: one that corresponds to the explicit inputs the referenced Component config entity (version) supports.<br>
(Prior art for this: <code>\Drupal\canvas\Config\Schema\JsonSchemaObject</code>, for validating code components' definition against a JSON Schema subset.)
</li><li>✅ Update path: <img src="https://www.drupal.org/files/issues/2026-04-28/Screenshot%202026-04-27%20at%202.44.38%E2%80%AFPM.png">
</li><li>✅ Introduce <code> ComponentTreeConfigEntityBase:: componentTreeInstancesInputsMustBeArrays()</code> and use it in <code>::set('component_tree', …)</code> + <code>::preCreate()</code> to convert the JSON blob assigned to a component instance's <code>inputs</code> (explicit inputs) to an array of key-value pairs (with the keys being the explicit inputs for which values are being specified).
</li><li>✅ Ensure that a config-defined component instance that populates an explicit input using structured data (<code>EntityFieldPropSource</code>, <code>HostEntityUrlPropSource</code>) correctly detects and prevents duplicating or overriding that structured data in a symmetrical translation. This is achieved using the same dynamic config schema: this MR's <code>ComponentInputsMapping extends Mapping</code> class takes into account the values for the <code>inputs</code> in the default translation: if it's populated by structured data, set <code>translatable: false</code>. <a href="https://git.drupalcode.org/project/canvas/-/merge_requests/868/diffs?commit_id=828828050e559c942aa404645135bfd8c7c331c8">Test coverage</a> was added for this.
</li><li>✅ Ensure that a config-defined component tree is symmetrically translatable using core's Config Translation UI:
<ul>
<li>For block component instances, reuse all the existing config translation infrastructure+UX that would be used for <code>block</code> config entities
</li><li>For SDC+code component component instances, specify for every input <code>form_element_class: CanvasStaticPropSourceFieldWidget extends FormElementBase</code> to generate a field widget-powered UX, and its submission handling will automatically use the same optimizing ("collapsing") for storage, resulting in correct overrides. → this means any prop shape or any specific prop can be marked as translatable, and it will automatically work.
</li>
</ul>
<p>→ <a href="https://git.drupalcode.org/project/canvas/-/merge_requests/898#note_747622">Details</a><br>
→ Screenshot:<br>
<img src="https://www.drupal.org/files/issues/2026-04-30/ai.test_admin_structure_content-template_node.something.full_translate__2_.png">
</p></li>
</ol>
<h4>Test coverage</h4>
<ol>
<li>✅ Kernel test coverage for configuration translation foundations in <code>\Drupal\Tests\canvas\Kernel\Config\ContentTemplateTest::testTranslationLifeCycleInDepth()</code></li>
<li>✅ Integration test coverage to prove it yields the expected results when rendering a component tree and visiting as an English and a French site visitor: <code>\Drupal\Tests\canvas\Functional\TranslationTest::testContentTemplateTranslationRendered()</code></li>
<li>✅ Integration test coverage to prove it is possible to generate valid config translations (<code>LanguageConfigOverride</code>s) using the UI: <code>TranslationTest::testContentTemplateConfigTranslationUi()</code>.
</li><li>✅ Update path test coverage, to prove that the config-defined component trees are updated correctly, both when >=1 config-defined component instance's <code>inputs</code> still is a JSON blob (typical scenario) and when it already is an array (atypical, but possible when e.g. using recipes, possible due to <code>type: ignore</code> not applying any transformation at saving time). : <code>ConfigComponentInputsMustBeArraysUpdateTest</code>.
</li></ol>
<h4>Out of scope</h4>
<ul>
<li>validation of config translations. Core's <code>LanguageConfigOverride</code> does not seem to have any validation at all. Created <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-1"><a href="https://www.drupal.org/project/canvas/issues/3583854" title="Status: Active">#3583854: [upstream] Validate LanguageConfigOverrides targeting Canvas config entities</a></span> for this.
</li>
<li>alter hook to opt in more prop shapes to become translatable: <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-4"><a href="https://www.drupal.org/project/canvas/issues/3584178" title="Status: Postponed">#3584178: [later phase] Add alter hook for marking additional SDC/code component prop shapes AND/OR specific props as translatable</a></span>
</li><li>TMGMT support → <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-13"><a href="https://www.drupal.org/project/canvas/issues/3582558" title="Status: Needs work">#3582558: Symmetrically translatable config-defined component trees, STEP 4: allow translating component instance inputs via TMGMT for ContentTemplates & PageRegions</a></span>
</li></ul>
<h3 id="ui-changes">User interface changes</h3>
<p>Page Regions and Content Templates become translatable in core's Config Translation UI. <img src="https://www.drupal.org/files/issues/2026-04-30/ai.test_admin_structure_content-template_node.something.full_translate__2_.png"></p>
> Related issue: [Issue #3568264](https://www.drupal.org/node/3568264)
> Related issue: [Issue #3582478](https://www.drupal.org/node/3582478)
> Related issue: [Issue #3520484](https://www.drupal.org/node/3520484)
> Related issue: [Issue #3583945](https://www.drupal.org/node/3583945)
> Related issue: [Issue #3584143](https://www.drupal.org/node/3584143)
> Related issue: [Issue #3584178](https://www.drupal.org/node/3584178)
> Related issue: [Issue #3586342](https://www.drupal.org/node/3586342)
> Related issue: [Issue #3590196](https://www.drupal.org/node/3590196)
issue