chore(Component sources): #3591727 Store each prop's translatability per component version so deleting an optional prop keeps config-defined component trees valid

Closes #3591727 (closed)

Summary

ComponentInputsMapping (introduced in #3582478 (closed)) validates every config-defined component tree's inputs, but it builds each component instance's input schema from the component's live JSON Schema (getMetadata()), not the version the instance actually references. So deleting an optional prop from an SDC or code component — which creates a new component version — made previously valid-and-unchanged config-defined trees (still pointing at the old version) report the removed input as '<prop>' is not a supported key. This is a regression for monolingual sites and a bug for multilingual sites.

Root cause

ComponentInputs::resolveConfigSchemaMapping()JsonSchemaPropsComponentInstanceInputsConfigSchemaGenerator::getConfigSchemaMapping()JsonSchemaPropsComponentSourceBase::getExplicitInputDefinitions()getMetadata(), which can only return the live/deployed schema. The mapping therefore omits props that the referenced version had but the live implementation no longer does.

Approach

Per the 2026-07-01 discussion (see !1300 (comment 1371450)): track translatability from the actual JSON schema shape, not a stored boolean, so it can be re-derived if the rules ever evolve.

  • Each prop's field definition gains a required derived_schema_metadata mapping (canvas.json_schema_props), computed at discovery while the live implementation is still available. It retains the prop's string_shape, reduced to the keys that matter to translation systems: pattern (multi-line), contentMediaType (rich text), format (URI-esque). The full JSON Schema cannot be stored: an array prop's items.meta:enum keys may contain dots, which config keys cannot hold.
  • Translatability is derived, never stored: retained string_shape present AND the prop's (versioned) expression is not reference-backed. The single rule lives in JsonSchemaPropsComponentSourceBase::isExplicitInputTranslatable() and is shared by the config schema generator and the TMGMT extractor, so the two never disagree.
  • getConfigSchemaMapping() now builds the mapping exclusively from the versioned prop_field_definitions — prop set, requiredKey and translatability never consult live metadata. (Only the cosmetic label still prefers the live title, falling back to the prop name; a stale title cannot invalidate stored config.)
  • derived_schema_metadata is excluded from the component version hash via the new ComponentSourceBase::settingsAffectingVersionHash() extension point (keeps the base class unaware of prop_field_definitions, cf. !1300 (comment 1306644)): it is fully derived from the already-hashed explicit-input schema, so recomputing or backfilling it never changes existing version ids.
  • Because the key is required, canvas_post_update_0021 + a Component::preSave() heal backfill it: live-derived for the active version, empty for past versions — what shape a prop had when a past version was created is unknowable, so those props are treated as not translatable; instances become translatable when automatic component instance updating moves them to the active version (cf. !1300 (comment 1371453)). No new versions are created.
  • A populated input without a field definition (e.g. a prop whose shape is not storable) stays valid when backed by a non-static prop source: nothing to translate here — the value may be translated at its source. A static source for an unknown prop remains rejected.
  • Fallout of making minLength unsupported (da27eebb): a required prop constrained only by minLength now makes its component ineligible. The incidental minLength: 2 is stripped from three test SDCs (their version hashes change throughout the test suite); the new props-min-length test SDC pins the ineligibility behavior explicitly, and PropShapeRepositoryTest moves type=string&minLength=2 to the unstorable shapes (core's sdc_test:my-button cannot be changed).

Testing steps

  • ddev xb-phpunit tests/src/Unit/PropShape/PropShapeGetTranslatableStringShapeTest.php
  • ddev xb-phpunit tests/src/Kernel/ApiAutoSaveControllerTranslationTest.php
  • ddev xb-phpunit tests/src/Kernel/Plugin/Canvas/ComponentSource/SingleDirectoryComponentTest.php
  • ddev xb-phpunit tests/src/Kernel/Plugin/Canvas/ComponentSource/JsComponentTest.php
  • ddev xb-phpunit tests/src/Kernel/PropShapeRepositoryTest.php
  • ddev xb-phpunit tests/src/Kernel/Config/ComponentValidationTest.php --filter testDerivedSchemaMetadataBackfilledOnSave
  • composer phpcs && composer phpstan
  • Manual: place a config-defined component tree (e.g. a PageRegion) using an SDC/code component with an optional string prop set; remove that prop from the component; confirm the region stays valid and its other inputs remain translatable (was: '<prop>' is not a supported key).

Notes for reviewers

  • ⚠️ Real-site impact of da27eebb worth a release note: an existing SDC with a required prop constrained only by minLength becomes ineligible on rediscovery. Previously the constraint was silently dropped (stored as a plain string, unenforced).
  • Adjacent observation, deliberately untouched: validateComponentInput()'s garbage check still flags inputs without a field definition as "the <prop> prop is not defined" for the default-language tree. It already reads versioned data, so it is version-correct; whether a dynamic source on a never-storable prop should also pass that layer is a separate decision.

AI usage disclosure

Per Drupal.org's policy on AI contributions: root-cause analysis, implementation, and test updates were produced with Claude (Opus 4.8, Fable 5) and reviewed by the human contributor before pushing.

Edited by Christian López Espínola

Merge request reports

Loading