Config actions
Config actions (such as createOrUpdate) create or modify config entities
programmatically. This page covers how langcodes are determined for entities
created via config actions, both standalone and inside recipes.
How Drupal determines langcode without a lock
Without a lock, config actions use the current request language as the langcode when no explicit value is given. If the action specifies a langcode explicitly, that value is used as-is.
ConfigActionManager validates FullyValidatable entity types after each
action. An entity type without the FullyValidatable schema marker is never
validated, so any langcode value is accepted silently.
How this module enforces the lock
hook_entity_presave intercepts every config entity save and overwrites the
langcode to the lock language. This fires before ConfigActionManager's
validation step, so:
- Invalid langcodes (empty string, unknown codes) are normalized before the save takes place.
- The saved entity always has a valid langcode.
FullyValidatablevalidation passes.
Config actions inside recipes are handled by hook_entity_presave and not
by the RecipeAppliedEvent subscriber. The subscriber only processes config
names from the recipe's static config storage (files in config/); entities
created at runtime by actions are not in that list.
Outcome matrix
The test module ships two entity types:
lock_test_always_valid— hasFullyValidatablein its schema; validated after each action.lock_test_maybe_invalid— noFullyValidatable; never validated.
In the tables below, xx is the request language, yy is a known installed
language that is neither the request language nor the site default, and de is
the lock language used in locked scenarios.
Standalone config action — no lock
| langcode in action | Entity type | Saved langcode | Result |
|---|---|---|---|
(none — request lang xx) |
always_valid | xx |
200 |
yy (explicit, known lang) |
always_valid | yy |
200 |
| (empty) | always_valid | (empty) ⚠ | 500 ⚠ — fails FullyValidatable, entity kept with bad langcode |
zz (unknown lang) |
always_valid | zz ⚠ |
500 ⚠ — fails FullyValidatable, entity kept with bad langcode |
(none — request lang xx) |
maybe_invalid | xx |
200 |
yy (explicit, known lang) |
maybe_invalid | yy |
200 |
| (empty) | maybe_invalid | (empty) ⚠ | 200 ⚠ — no FullyValidatable, bad langcode silently kept |
zz (unknown lang) |
maybe_invalid | zz ⚠ |
200 ⚠ — no FullyValidatable, bad langcode silently kept |
⚠ Invalid or empty langcode persists in storage — either because FullyValidatable
validation fires after the save (leaving the entity in the database with the bad
langcode before aborting with 500), or because maybe_invalid has no validation
at all and the bad value is silently accepted.
Standalone config action — lock = de
| langcode in action | Entity type | Saved langcode | Result |
|---|---|---|---|
(none — request lang xx) |
always_valid | de |
200 |
yy (explicit) |
always_valid | de |
200 |
| (empty) | always_valid | de |
200 — lock normalizes before save |
zz (unknown lang) |
always_valid | de |
200 — lock normalizes before save |
(none — request lang xx) |
maybe_invalid | de |
200 |
yy (explicit) |
maybe_invalid | de |
200 |
| (empty) | maybe_invalid | de |
200 — lock normalizes before save |
zz (unknown lang) |
maybe_invalid | de |
200 — lock normalizes before save |
Config actions inside a recipe — no lock
The same rules apply as standalone. The recipe subscriber does not re-process
action-created entities; hook_entity_presave handles them at save time.
| Entity | Action langcode | Saved langcode |
|---|---|---|
lock_action_always_valid |
(none — request lang xx) |
xx |
lock_action_always_valid_other |
yy (explicit, known lang) |
yy |
lock_action_maybe_invalid_missing |
(none — request lang xx) |
xx |
lock_action_maybe_invalid_empty |
(empty) | (empty) ⚠ — no validation, bad langcode silently kept |
lock_action_maybe_invalid_unknown |
zz (unknown lang) |
zz ⚠ — no validation, bad langcode silently kept |
lock_action_empty |
(empty) | (empty) ⚠ — then 500 ⚠, entity kept with bad langcode |
lock_action_unknown |
zz (unknown lang) |
zz ⚠ — then 500 ⚠, entity kept with bad langcode |
Config actions inside a recipe — lock = de
| Entity | Action langcode | Saved langcode |
|---|---|---|
lock_action_always_valid |
(none — request lang xx) |
de |
lock_action_always_valid_other |
yy (explicit) |
de |
lock_action_maybe_invalid_missing |
(none — request lang xx) |
de |
lock_action_maybe_invalid_empty |
(empty) | de — lock normalizes before save |
lock_action_maybe_invalid_unknown |
zz (unknown lang) |
de — lock normalizes before save |
lock_action_empty |
(empty) | de — lock normalizes before save |
lock_action_unknown |
zz (unknown lang) |
de — lock normalizes before save |
Flow diagram
flowchart TD
A[Config action fires] --> B{Lock language set?}
B -- No --> C{Langcode empty\nin action?}
C -- Yes --> D[Langcode = request language]
C -- No --> E[Langcode = explicit value from action]
D --> F{FullyValidatable\nin schema?}
E --> F
F -- No --> G[Done — any langcode accepted]
F -- Yes --> H{Langcode valid?}
H -- Yes --> I[FullyValidatable validation passes\n200 OK]
H -- No --> J[Entity saved with bad langcode ⚠\nthen 500 error ⚠]
B -- Yes --> K[hook_entity_presave overwrites\nlangcode → lock language before save]
K --> L[FullyValidatable validation passes\nif applicable — 200 OK]
Related pages
- UI forms — form-based config entity creation
- Recipes — direct config files and extension installs via recipes
- Module install — module install langcode behavior