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_metadatamapping (canvas.json_schema_props), computed at discovery while the live implementation is still available. It retains the prop'sstring_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'sitems.meta:enumkeys may contain dots, which config keys cannot hold. - Translatability is derived, never stored: retained
string_shapepresent AND the prop's (versioned)expressionis not reference-backed. The single rule lives inJsonSchemaPropsComponentSourceBase::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 versionedprop_field_definitions— prop set,requiredKeyand translatability never consult live metadata. (Only the cosmeticlabelstill prefers the live title, falling back to the prop name; a stale title cannot invalidate stored config.)derived_schema_metadatais excluded from the component version hash via the newComponentSourceBase::settingsAffectingVersionHash()extension point (keeps the base class unaware ofprop_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+ aComponent::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
minLengthunsupported (da27eebb): a required prop constrained only byminLengthnow makes its component ineligible. The incidentalminLength: 2is stripped from three test SDCs (their version hashes change throughout the test suite); the newprops-min-lengthtest SDC pins the ineligibility behavior explicitly, andPropShapeRepositoryTestmovestype=string&minLength=2to the unstorable shapes (core'ssdc_test:my-buttoncannot 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 byminLengthbecomes 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.