From 063b423e185e612b23ebef5e53b6e1d9f75f1221 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:30:18 +0200 Subject: [PATCH 1/4] feat: add language_hierarchy for multilingual translation fallback Add the language_hierarchy module to the multilingual recipe so that untranslated content falls back through a configured language chain instead of showing empty listings or picking an arbitrary translation. All languages fall back to English by default; nl-be (Vlaams) falls back to Dutch first. The en-gb footer translation is removed since it is identical to the English original and now falls back naturally. Views with interface-language filters (blog, case_studies, news) are automatically swapped to the hierarchy fallback filter on install via hook_install in dxpr_cms_multilingual_content. Also adds nl-be language entity config with label "Vlaams", removes obsolete translated menu link content for deleted pages, and adds the nl-be homepage translation. --- composer.json | 1 + recipes/dxpr_cms_multilingual/composer.json | 3 +- .../config/language.entity.nl-be.yml | 11 + .../5eca7429-4952-438e-b2f0-769f177aa5c7.yml | 217 ++++-------------- .../2bea87b5-6d87-47ff-9fa9-94a633d8e1aa.yml | 208 ----------------- .../5eb209b8-2a82-479f-921b-5448ec6d4838.yml | 208 ----------------- .../760d3cb1-da3d-46e4-b9f0-e76b1d3d973a.yml | 208 ----------------- .../564f412f-1a80-4a87-840b-d38edeeed383.yml | 42 ++++ recipes/dxpr_cms_multilingual/recipe.yml | 15 ++ .../dxpr_cms_multilingual_content.info.yml | 1 + .../dxpr_cms_multilingual_content.install | 76 ++++++ 11 files changed, 188 insertions(+), 802 deletions(-) create mode 100644 recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml delete mode 100644 recipes/dxpr_cms_multilingual/content/menu_link_content/2bea87b5-6d87-47ff-9fa9-94a633d8e1aa.yml delete mode 100644 recipes/dxpr_cms_multilingual/content/menu_link_content/5eb209b8-2a82-479f-921b-5448ec6d4838.yml delete mode 100644 recipes/dxpr_cms_multilingual/content/menu_link_content/760d3cb1-da3d-46e4-b9f0-e76b1d3d973a.yml create mode 100644 web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install diff --git a/composer.json b/composer.json index 0f593302a..feb4fbbee 100644 --- a/composer.json +++ b/composer.json @@ -146,6 +146,7 @@ "drupal/dxpr_cms_starter": "*", "drupal/easy_email_express": "^1", "drupal/google_tag": "^2.0", + "drupal/language_hierarchy": "^2.0", "drupal/project_browser": "^2.1", "drupal/recipe_installer_kit": "^1.0", "drush/drush": "^13" diff --git a/recipes/dxpr_cms_multilingual/composer.json b/recipes/dxpr_cms_multilingual/composer.json index 65c75b24a..4badcc829 100644 --- a/recipes/dxpr_cms_multilingual/composer.json +++ b/recipes/dxpr_cms_multilingual/composer.json @@ -11,7 +11,8 @@ "drupal/lang_dropdown": "^2.4", "drupal/tmgmt": "^1.15", "drupal/ai_tmgmt": "^1.0.0-beta5", - "drupal/ai_translate": "^1.3" + "drupal/ai_translate": "^1.3", + "drupal/language_hierarchy": "^2.0" }, "version": "1.0.0-beta1" } diff --git a/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml new file mode 100644 index 000000000..089a4844e --- /dev/null +++ b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml @@ -0,0 +1,11 @@ +langcode: en +status: true +dependencies: { } +third_party_settings: + language_hierarchy: + fallback_langcode: nl +id: nl-be +label: Vlaams +direction: ltr +weight: 0 +locked: false diff --git a/recipes/dxpr_cms_multilingual/content/block_content/5eca7429-4952-438e-b2f0-769f177aa5c7.yml b/recipes/dxpr_cms_multilingual/content/block_content/5eca7429-4952-438e-b2f0-769f177aa5c7.yml index ce2dd841b..262f21cd2 100644 --- a/recipes/dxpr_cms_multilingual/content/block_content/5eca7429-4952-438e-b2f0-769f177aa5c7.yml +++ b/recipes/dxpr_cms_multilingual/content/block_content/5eca7429-4952-438e-b2f0-769f177aa5c7.yml @@ -35,16 +35,11 @@ default:
-
+
DXPR logo

© 2026 DXPR. All rights reserved.

- -
+

Contact

Contact us

@@ -73,16 +68,11 @@ translations:
-
+
DXPR logo

© 2026 DXPR. Alle rechten voorbehouden.

- -
+

Contact

Neem contact op

@@ -110,16 +100,11 @@ translations:
-
+
DXPR logo

© 2026 DXPR. Todos los derechos reservados.

- -
+

Contacto

Contáctenos

@@ -128,43 +113,6 @@ translations:
format: dxpr_builder_html summary: '' - en-gb: - status: - - - value: true - info: - - - value: Footer - content_translation_source: - - - value: en - content_translation_outdated: - - - value: false - body: - - - value: | -
-
-
-
- DXPR logo -

© 2026 DXPR. All rights reserved.

-
- -
-

Contact

-

Contact us

-
-
-
-
- format: dxpr_builder_html - summary: '' de: status: - @@ -184,16 +132,11 @@ translations:
-
+
DXPR-Logo

© 2026 DXPR. Alle Rechte vorbehalten.

- -
+ @@ -221,16 +164,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Tous droits réservés.

- -
+

Contact

Contactez-nous

@@ -258,16 +196,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Tutti i diritti riservati.

- -
+

Contatto

Contattaci

@@ -295,16 +228,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Wszelkie prawa zastrzeżone.

- -
+ @@ -332,16 +260,11 @@ translations:
-
+
DXPR-logotyp

© 2026 DXPR. Alla rättigheter förbehållna.

- -
+

Kontakt

Kontakta oss

@@ -369,16 +292,11 @@ translations:
-
+
DXPR-logo

© 2026 DXPR. Alle rettigheder forbeholdes.

- -
+

Kontakt

Kontakt os

@@ -406,16 +324,11 @@ translations:
-
+
DXPR-logo

© 2026 DXPR. Alle rettigheter forbeholdt.

- -
+

Kontakt

Kontakt oss

@@ -443,16 +356,11 @@ translations:
-
+
DXPR-logo

© 2026 DXPR. Kaikki oikeudet pidätetään.

- -
+

Yhteystiedot

Ota yhteyttä

@@ -480,16 +388,11 @@ translations:
-
+
Logótipo DXPR

© 2026 DXPR. Todos os direitos reservados.

- -
+

Contacto

Contacte-nos

@@ -517,16 +420,11 @@ translations:
-
+
DXPRロゴ

© 2026 DXPR. All rights reserved.

- -
+

お問い合わせ

お問い合わせ

@@ -554,16 +452,11 @@ translations:
-
+
شعار DXPR

© 2026 DXPR. جميع الحقوق محفوظة.

- -
+

اتصل بنا

تواصل معنا

@@ -591,16 +484,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Všechna práva vyhrazena.

- -
+ @@ -628,16 +516,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Toate drepturile rezervate.

- -
+

Contact

Contactați-ne

@@ -665,16 +548,11 @@ translations:
-
+
DXPR logosu

© 2026 DXPR. Tüm hakları saklıdır.

- -
+

İletişim

Bize ulaşın

@@ -702,16 +580,11 @@ translations:
-
+
DXPR 로고

© 2026 DXPR. All rights reserved.

- -
+

문의

문의하기

@@ -739,16 +612,11 @@ translations:
-
+
Logo DXPR

© 2026 DXPR. Hak cipta dilindungi.

- -
+

Kontak

Hubungi kami

@@ -776,16 +644,11 @@ translations:
-
+
DXPR 标志

© 2026 DXPR. All rights reserved.

- -
+

联系我们

联系我们

diff --git a/recipes/dxpr_cms_multilingual/content/menu_link_content/2bea87b5-6d87-47ff-9fa9-94a633d8e1aa.yml b/recipes/dxpr_cms_multilingual/content/menu_link_content/2bea87b5-6d87-47ff-9fa9-94a633d8e1aa.yml deleted file mode 100644 index 81c044e87..000000000 --- a/recipes/dxpr_cms_multilingual/content/menu_link_content/2bea87b5-6d87-47ff-9fa9-94a633d8e1aa.yml +++ /dev/null @@ -1,208 +0,0 @@ -_meta: - version: '1.0' - entity_type: menu_link_content - uuid: 2bea87b5-6d87-47ff-9fa9-94a633d8e1aa - bundle: menu_link_content - default_langcode: en -default: - enabled: - - - value: true - title: - - - value: 'Demo guide' - menu_name: - - - value: main - link: - - - uri: 'internal:/guide' - title: '' - options: {} - weight: - - - value: 0 - expanded: - - - value: false -translations: - nl: - title: - - - value: Demogids - link: - - - uri: 'internal:/guide' - title: '' - options: {} - es: - title: - - - value: "Gu\xEDa de demostraci\xF3n" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - en-gb: - title: - - - value: "Demo guide" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - de: - title: - - - value: "Demo-Anleitung" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - fr: - title: - - - value: "Guide de démonstration" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - it: - title: - - - value: "Guida demo" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - pl: - title: - - - value: "Przewodnik demo" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - sv: - title: - - - value: "Demoguide" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - da: - title: - - - value: "Demovejledning" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - nb: - title: - - - value: "Demoguide" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - fi: - title: - - - value: "Demo-opas" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - pt-pt: - title: - - - value: "Guia de demonstração" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - ja: - title: - - - value: "デモガイド" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - ar: - title: - - - value: "دليل العرض" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - cs: - title: - - - value: "Průvodce ukázkou" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - ro: - title: - - - value: "Ghid demo" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - tr: - title: - - - value: "Demo rehberi" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - ko: - title: - - - value: "데모 가이드" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - id: - title: - - - value: "Panduan demo" - link: - - - uri: 'internal:/guide' - title: '' - options: {} - zh-hans: - title: - - - value: "演示指南" - link: - - - uri: 'internal:/guide' - title: '' - options: {} diff --git a/recipes/dxpr_cms_multilingual/content/menu_link_content/5eb209b8-2a82-479f-921b-5448ec6d4838.yml b/recipes/dxpr_cms_multilingual/content/menu_link_content/5eb209b8-2a82-479f-921b-5448ec6d4838.yml deleted file mode 100644 index d5d7f3893..000000000 --- a/recipes/dxpr_cms_multilingual/content/menu_link_content/5eb209b8-2a82-479f-921b-5448ec6d4838.yml +++ /dev/null @@ -1,208 +0,0 @@ -_meta: - version: '1.0' - entity_type: menu_link_content - uuid: 5eb209b8-2a82-479f-921b-5448ec6d4838 - bundle: menu_link_content - default_langcode: en -default: - enabled: - - - value: true - title: - - - value: 'What''s new' - menu_name: - - - value: main - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - weight: - - - value: 1 - expanded: - - - value: false -translations: - nl: - title: - - - value: Wat is nieuw - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - es: - title: - - - value: Novedades - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - en-gb: - title: - - - value: "What's new" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - de: - title: - - - value: "Neuigkeiten" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - fr: - title: - - - value: "Nouveautés" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - it: - title: - - - value: "Novità" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - pl: - title: - - - value: "Co nowego" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - sv: - title: - - - value: "Nyheter" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - da: - title: - - - value: "Nyheder" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - nb: - title: - - - value: "Hva er nytt" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - fi: - title: - - - value: "Uutta" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - pt-pt: - title: - - - value: "Novidades" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - ja: - title: - - - value: "新機能" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - ar: - title: - - - value: "ما الجديد" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - cs: - title: - - - value: "Co je nového" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - ro: - title: - - - value: "Noutăți" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - tr: - title: - - - value: "Yenilikler" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - ko: - title: - - - value: "새 소식" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - id: - title: - - - value: "Yang baru" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} - zh-hans: - title: - - - value: "最新动态" - link: - - - uri: 'internal:/whats-new' - title: '' - options: {} diff --git a/recipes/dxpr_cms_multilingual/content/menu_link_content/760d3cb1-da3d-46e4-b9f0-e76b1d3d973a.yml b/recipes/dxpr_cms_multilingual/content/menu_link_content/760d3cb1-da3d-46e4-b9f0-e76b1d3d973a.yml deleted file mode 100644 index f740000ff..000000000 --- a/recipes/dxpr_cms_multilingual/content/menu_link_content/760d3cb1-da3d-46e4-b9f0-e76b1d3d973a.yml +++ /dev/null @@ -1,208 +0,0 @@ -_meta: - version: '1.0' - entity_type: menu_link_content - uuid: 760d3cb1-da3d-46e4-b9f0-e76b1d3d973a - bundle: menu_link_content - default_langcode: en -default: - enabled: - - - value: true - title: - - - value: 'About DXPR' - menu_name: - - - value: main - link: - - - uri: 'internal:/about' - title: '' - options: {} - weight: - - - value: 2 - expanded: - - - value: false -translations: - nl: - title: - - - value: Over DXPR - link: - - - uri: 'internal:/about' - title: '' - options: {} - es: - title: - - - value: Acerca de DXPR - link: - - - uri: 'internal:/about' - title: '' - options: {} - en-gb: - title: - - - value: "About DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - de: - title: - - - value: "Über DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - fr: - title: - - - value: "À propos de DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - it: - title: - - - value: "Informazioni su DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - pl: - title: - - - value: "O DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - sv: - title: - - - value: "Om DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - da: - title: - - - value: "Om DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - nb: - title: - - - value: "Om DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - fi: - title: - - - value: "Tietoa DXPR:stä" - link: - - - uri: 'internal:/about' - title: '' - options: {} - pt-pt: - title: - - - value: "Sobre o DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - ja: - title: - - - value: "DXPRについて" - link: - - - uri: 'internal:/about' - title: '' - options: {} - ar: - title: - - - value: "حول DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - cs: - title: - - - value: "O DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - ro: - title: - - - value: "Despre DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - tr: - title: - - - value: "DXPR Hakkında" - link: - - - uri: 'internal:/about' - title: '' - options: {} - ko: - title: - - - value: "DXPR 소개" - link: - - - uri: 'internal:/about' - title: '' - options: {} - id: - title: - - - value: "Tentang DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} - zh-hans: - title: - - - value: "关于 DXPR" - link: - - - uri: 'internal:/about' - title: '' - options: {} diff --git a/recipes/dxpr_cms_multilingual/content/node/564f412f-1a80-4a87-840b-d38edeeed383.yml b/recipes/dxpr_cms_multilingual/content/node/564f412f-1a80-4a87-840b-d38edeeed383.yml index c81165621..6609b4676 100644 --- a/recipes/dxpr_cms_multilingual/content/node/564f412f-1a80-4a87-840b-d38edeeed383.yml +++ b/recipes/dxpr_cms_multilingual/content/node/564f412f-1a80-4a87-840b-d38edeeed383.yml @@ -133,6 +133,48 @@ translations: field_dth_hide_regions: - value: page_title + nl-be: + status: + - + value: true + uid: + - + target_id: 1 + title: + - + value: "Thuis" + created: + - + value: 1680675002 + promote: + - + value: true + sticky: + - + value: false + moderation_state: + - + value: published + path: + - + alias: /home + langcode: nl-be + pathauto: 0 + content_translation_source: + - + value: en + content_translation_outdated: + - + value: false + field_content: + - + value: | +
DXPR logo

Den AI-native Drupal
paginabouwer is geland!

Vertelt wa ge wilt, en kijkt hoe DXPR AI
da proper in mekaar steekt met structuur, best practices en precisie.

DXPR Builder 3 is van de grond af herwerkt voor de volgende generatie Drupal-contentcreatie. Da's pas proper werk.

Kijkt hoe AI uw pagina in mekaar bokst. Vijlt elke sectie visueel bij. En publiceert dan gewoon met de Drupal-rechten, revisies, vertalingen en workflows waar uw organisatie al op draait.

Van idee naar pagina, plansen maar

Genereert complete pagina's, herschrijft secties, vertaalt content, maakt afbeeldingen en bouwt herbruikbare lay-outs zonder van een wit blad te moeten vertrekken.

DXPR Builder UI: AI-generatieprompt

Bewerken met goesting

Een vernieuwde interface gericht op WCAG 2.2 AAA geeft contentteams een properder, rapper en veel toegankelijker manier van bouwen.

DXPR Builder UI: AI-chat

Drupal blijft de baas

Uw content blijft gestructureerd, overdraagbaar en beheerd door Drupal, met propere Bootstrap 5 HTML in plaats van ne hoop eigen paginabouwer-gedoe.

DXPR Builder UI: Inline bewerken
+ format: dxpr_builder_html + summary: '' + field_dth_hide_regions: + - + value: page_title zh-hans: status: - diff --git a/recipes/dxpr_cms_multilingual/recipe.yml b/recipes/dxpr_cms_multilingual/recipe.yml index 9ab001c0d..203228e1e 100644 --- a/recipes/dxpr_cms_multilingual/recipe.yml +++ b/recipes/dxpr_cms_multilingual/recipe.yml @@ -21,6 +21,7 @@ install: - tmgmt_config - ai_tmgmt - ai_translate + - language_hierarchy - user - dxpr_cms_multilingual_content config: @@ -72,6 +73,20 @@ config: key: bundle_settings value: untranslatable_fields_hide: '1' + # Set all languages to fall back to English by default. + language.entity.*: + setThirdPartySettings: + - + module: language_hierarchy + key: fallback_langcode + value: en + # Override nl-be to fall back to Dutch first. + language.entity.nl-be: + setThirdPartySettings: + - + module: language_hierarchy + key: fallback_langcode + value: nl ai_translate.settings: simpleConfigUpdate: reference_defaults: diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.info.yml b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.info.yml index f3de62cd9..04dcd6d68 100644 --- a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.info.yml +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.info.yml @@ -5,3 +5,4 @@ core_version_requirement: ^11 package: DXPR CMS dependencies: - drupal:content_translation + - drupal:language_hierarchy diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install new file mode 100644 index 000000000..04c8267eb --- /dev/null +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install @@ -0,0 +1,76 @@ +moduleExists('language_hierarchy')) { + return; + } + + $view_storage = \Drupal::entityTypeManager()->getStorage('view'); + $views = $view_storage->loadMultiple(); + + foreach ($views as $view) { + $display = $view->get('display'); + $changed = FALSE; + + foreach ($display as $display_id => &$display_config) { + $filters = $display_config['display_options']['filters'] ?? []; + + foreach ($filters as $filter_id => $filter) { + if (($filter['plugin_id'] ?? '') !== 'language' + || ($filter['entity_field'] ?? '') !== 'langcode') { + continue; + } + + $value = $filter['value'] ?? []; + $is_interface = is_array($value) + && isset($value['***LANGUAGE_language_interface***']); + + if (!$is_interface) { + continue; + } + + unset($display_config['display_options']['filters'][$filter_id]); + + $display_config['display_options']['filters']['language_hierarchy_content_language_fallback_limited_langcode'] = [ + 'id' => 'language_hierarchy_content_language_fallback_limited_langcode', + 'table' => $filter['table'], + 'field' => 'language_hierarchy_content_language_fallback_limited_langcode', + 'relationship' => 'none', + 'group_type' => 'group', + 'admin_label' => '', + 'plugin_id' => 'language_hierarchy_content_language_fallback_limited', + 'value' => '1', + 'group' => $filter['group'] ?? 1, + 'exposed' => FALSE, + 'is_grouped' => FALSE, + ]; + $changed = TRUE; + } + } + unset($display_config); + + if ($changed) { + $deps = $view->get('dependencies'); + if (!in_array('language_hierarchy', $deps['module'] ?? [], TRUE)) { + $deps['module'][] = 'language_hierarchy'; + $view->set('dependencies', $deps); + } + $view->set('display', $display); + $view->save(); + } + } +} From c63acdb2ffb2f871e5cc800a21713cbf48356ed6 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:59:09 +0200 Subject: [PATCH 2/4] fix: set language fallback chains programmatically during install The recipe config action `language.entity.*` with setThirdPartySettings does not apply to language entities created by the installer, because the multilingual form batch runs after recipe application. Move fallback chain configuration into hook_install (for languages existing at module install time) and hook_configurable_language_insert (for languages created afterwards by the installer batch). --- recipes/dxpr_cms_multilingual/recipe.yml | 14 -------- .../dxpr_cms_multilingual_content.install | 32 +++++++++++++++++++ .../dxpr_cms_multilingual_content.module | 23 +++++++++++++ 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/recipes/dxpr_cms_multilingual/recipe.yml b/recipes/dxpr_cms_multilingual/recipe.yml index 203228e1e..220ac3449 100644 --- a/recipes/dxpr_cms_multilingual/recipe.yml +++ b/recipes/dxpr_cms_multilingual/recipe.yml @@ -73,20 +73,6 @@ config: key: bundle_settings value: untranslatable_fields_hide: '1' - # Set all languages to fall back to English by default. - language.entity.*: - setThirdPartySettings: - - - module: language_hierarchy - key: fallback_langcode - value: en - # Override nl-be to fall back to Dutch first. - language.entity.nl-be: - setThirdPartySettings: - - - module: language_hierarchy - key: fallback_langcode - value: nl ai_translate.settings: simpleConfigUpdate: reference_defaults: diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install index 04c8267eb..c2ed01ef7 100644 --- a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install @@ -19,6 +19,38 @@ function dxpr_cms_multilingual_content_install(): void { return; } + _dxpr_cms_multilingual_content_configure_fallback_chains(); + _dxpr_cms_multilingual_content_swap_views_filters(); +} + +/** + * Configures language_hierarchy fallback chains for all installed languages. + */ +function _dxpr_cms_multilingual_content_configure_fallback_chains(): void { + $overrides = [ + 'nl-be' => 'nl', + ]; + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + + $languages = \Drupal::entityTypeManager() + ->getStorage('configurable_language') + ->loadMultiple(); + + foreach ($languages as $language) { + $langcode = $language->id(); + if ($langcode === $default_langcode) { + continue; + } + $fallback = $overrides[$langcode] ?? $default_langcode; + $language->setThirdPartySetting('language_hierarchy', 'fallback_langcode', $fallback); + $language->save(); + } +} + +/** + * Swaps standard language filters in Views with language_hierarchy fallback. + */ +function _dxpr_cms_multilingual_content_swap_views_filters(): void { $view_storage = \Drupal::entityTypeManager()->getStorage('view'); $views = $view_storage->loadMultiple(); diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module index a23343ea2..bf5fe335e 100644 --- a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module @@ -18,6 +18,10 @@ use Drupal\dxpr_cms_multilingual_content\EventSubscriber\TranslationMergeSubscri * existing entities. */ function dxpr_cms_multilingual_content_configurable_language_insert(ConfigurableLanguageInterface $language): void { + if (\Drupal::moduleHandler()->moduleExists('language_hierarchy')) { + _dxpr_cms_multilingual_content_set_language_fallback($language); + } + try { $data = TranslationMergeSubscriber::loadRecipeContentData(); if (empty($data)) { @@ -34,3 +38,22 @@ function dxpr_cms_multilingual_content_configurable_language_insert(Configurable ); } } + +/** + * Sets the language_hierarchy fallback for a language entity. + */ +function _dxpr_cms_multilingual_content_set_language_fallback(ConfigurableLanguageInterface $language): void { + $overrides = [ + 'nl-be' => 'nl', + ]; + $langcode = $language->id(); + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + + if ($langcode === $default_langcode) { + return; + } + + $fallback = $overrides[$langcode] ?? $default_langcode; + $language->setThirdPartySetting('language_hierarchy', 'fallback_langcode', $fallback); + $language->save(); +} From 55355f794c63b1af46f7a82aae2511944b26b822 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:05:32 +0200 Subject: [PATCH 3/4] fix: resolve emdash-lint and yamllint CI failures Replace em dashes with colons/semicolons in skill markdown files and fix excess spaces inside empty braces in recipe YAML configs. --- .agents/skills/drush-webmaster/SKILL.md | 24 ++++----- .agents/skills/dxb/SKILL.md | 14 ++--- .agents/skills/dxb/agents/openai.yaml | 2 +- .agents/skills/dxt/SKILL.md | 2 +- .../config/language.entity.nl-be.yml | 2 +- .../config/editor.editor.easy_email.yml | 2 +- .../config/filter.format.easy_email.yml | 16 +++--- ...il.user_created_approval_admin.default.yml | 34 ++++++------ ...il.user_created_approval_admin.default.yml | 54 +++++++++---------- ..._email.easy_email_type.user_activation.yml | 10 ++-- ...asy_email.easy_email_type.user_blocked.yml | 10 ++-- ...sy_email.easy_email_type.user_canceled.yml | 10 ++-- ...il_type.user_cancellation_confirmation.yml | 10 ++-- ...email_type.user_created_approval_admin.yml | 8 +-- ...il_type.user_created_awaiting_approval.yml | 10 ++-- ....easy_email_type.user_created_by_admin.yml | 10 ++-- ...sy_email_type.user_created_no_approval.yml | 10 ++-- ...easy_email_type.user_password_recovery.yml | 10 ++-- ...de.easy_email_override.user_activation.yml | 2 +- ...rride.easy_email_override.user_blocked.yml | 2 +- ...ride.easy_email_override.user_canceled.yml | 2 +- ...verride.user_cancellation_confirmation.yml | 2 +- ...l_override.user_created_approval_admin.yml | 2 +- ...verride.user_created_awaiting_approval.yml | 2 +- ...y_email_override.user_created_by_admin.yml | 2 +- ...mail_override.user_created_no_approval.yml | 2 +- ..._email_override.user_password_recovery.yml | 2 +- ...ated_approval_admin.field_related_user.yml | 2 +- ....storage.easy_email.field_related_user.yml | 2 +- .../easy_email.easy_email_type.default.yml | 10 ++-- ...l_override.easy_email_override.default.yml | 4 +- 31 files changed, 137 insertions(+), 137 deletions(-) diff --git a/.agents/skills/drush-webmaster/SKILL.md b/.agents/skills/drush-webmaster/SKILL.md index ea34f5bce..094871268 100644 --- a/.agents/skills/drush-webmaster/SKILL.md +++ b/.agents/skills/drush-webmaster/SKILL.md @@ -22,7 +22,7 @@ full documentation on any command before using it. ## Discovery (Start Here) -### `wm:schema:dump` — Full site schema for AI context +### `wm:schema:dump`: Full site schema for AI context **Run this first** when working with an unfamiliar site or section. @@ -35,7 +35,7 @@ drush wm:schema:dump --section=content-types --section=vocabularies Available sections: `content-types`, `vocabularies`, `views`, `menus`, `blocks`, `media-types`, `entity-types`, `site` -### `wm:site:info` — Site name, slogan, email, paths +### `wm:site:info`: Site name, slogan, email, paths ``` drush wm:site:info @@ -48,21 +48,21 @@ appropriate discovery command. This prevents validation errors. | Operation | Run first | |-----------|-----------| -| Create entity | `wm:content-type:get ` — required fields | -| Edit entity | `wm:entity:get ` — current values | -| Delete entity | `wm:entity:get ` — confirm correct entity | +| Create entity | `wm:content-type:get `: required fields | +| Edit entity | `wm:entity:get `: current values | +| Delete entity | `wm:entity:get `: confirm correct entity | | Add field to bundle | `wm:field:list ` then `wm:field:types` | | Create/edit view | `wm:view:tables` then `wm:view:available:fields` | -| Translate entity | `wm:translation:languages` — see configured languages | -| Moderate entity | `wm:entity:transitions ` — transitions | -| Bulk operations | `wm:content-type:get` or `wm:entity:query` — scope | +| Translate entity | `wm:translation:languages`: see configured languages | +| Moderate entity | `wm:entity:transitions `: transitions | +| Bulk operations | `wm:content-type:get` or `wm:entity:query`: scope | Skip discovery when context is already known or the user asks to proceed directly. ## Querying & Inspecting Entities -### `wm:entity:query` — Find entities by field conditions +### `wm:entity:query`: Find entities by field conditions ``` drush wm:entity:query node article \ @@ -76,14 +76,14 @@ drush wm:entity:query node blog --limit=50 --offset=50 Where operators: `=`, `!=`, `>`, `<`, `>=`, `<=`, `=NULL`, `!=NULL`, `=a,b,c` (IN list) -### `wm:entity:get` — Get single entity with field values +### `wm:entity:get`: Get single entity with field values ``` drush wm:entity:get node 1 drush wm:entity:get node 1 --fields=title,body ``` -### `wm:entity:field:get` / `wm:entity:field:set` — Get/set single field value +### `wm:entity:field:get` / `wm:entity:field:set`: Get/set single field value ``` drush wm:entity:field:get node 1 title @@ -92,7 +92,7 @@ drush wm:entity:field:set node 1 body \ '{"value":"

HTML

","format":"full_html"}' ``` -### `wm:search` — Full-text search (requires Search module) +### `wm:search`: Full-text search (requires Search module) ``` drush wm:search "contact form" diff --git a/.agents/skills/dxb/SKILL.md b/.agents/skills/dxb/SKILL.md index 6a4f77dfc..d5bc8a792 100644 --- a/.agents/skills/dxb/SKILL.md +++ b/.agents/skills/dxb/SKILL.md @@ -2,7 +2,7 @@ name: dxb description: | Build and manage DXPR Builder pages via drush dxb:* commands. - Write Bootstrap 5 HTML — the builder parses it into drag-and-drop + Write Bootstrap 5 HTML: the builder parses it into drag-and-drop editable elements. Use when asked to "create a page", "build a landing page", "edit page markup", "list templates", "update page layout", "create a reusable section", "scaffold content from a @@ -11,7 +11,7 @@ description: | # DXPR Builder Drush Commands - + ## HTML Content Rules DXPR Builder parses standard Bootstrap 5 HTML into drag-and-drop @@ -20,7 +20,7 @@ editable elements. Follow these rules when generating markup: ### Layout - Use Bootstrap grid system and responsive design patterns -- Row columns must total exactly 12 (e.g. col-6 + col-6, col-4 + col-4 + col-4). Do NOT use col-auto or col without a size — each column must have an explicit number (col-1 through col-12) +- Row columns must total exactly 12 (e.g. col-6 + col-6, col-4 + col-4 + col-4). Do NOT use col-auto or col without a size: each column must have an explicit number (col-1 through col-12) - Avoid nested containers - Sections contain content directly - no empty wrapper divs: - But only use a container-fluid when appropriate for the type of content @@ -58,10 +58,10 @@ Run `drush --help` for full options. ## Workflow -1. **Discover** — `template:list`, `element:list` -2. **Create** — `page:create` from template or create entity then `page:update` -3. **Edit** — `page:get` to read, modify, `page:update` to save -4. **Reuse** — `user-template:create` to save sections +1. **Discover**: `template:list`, `element:list` +2. **Create**: `page:create` from template or create entity then `page:update` +3. **Edit**: `page:get` to read, modify, `page:update` to save +4. **Reuse**: `user-template:create` to save sections Write Bootstrap 5 HTML following the rules above. The builder parses it into drag-and-drop components automatically. diff --git a/.agents/skills/dxb/agents/openai.yaml b/.agents/skills/dxb/agents/openai.yaml index 08b75b94d..4363735c5 100644 --- a/.agents/skills/dxb/agents/openai.yaml +++ b/.agents/skills/dxb/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "dxb" - short_description: "Build and manage DXPR Builder pages via drush dxb:* commands. Write Bootstrap 5 HTML — the builder parses it into drag-and-drop editable elements." + short_description: "Build and manage DXPR Builder pages via drush dxb:* commands. Write Bootstrap 5 HTML: the builder parses it into drag-and-drop editable elements." default_prompt: "Use dxb for page building, template management, and content layout tasks." policy: allow_implicit_invocation: true diff --git a/.agents/skills/dxt/SKILL.md b/.agents/skills/dxt/SKILL.md index d0f087653..b2057f36e 100644 --- a/.agents/skills/dxt/SKILL.md +++ b/.agents/skills/dxt/SKILL.md @@ -50,7 +50,7 @@ Keys: base, basetext, link, accent1, accent1text, accent2, accent2text, text, he ## Theme vs Content Responsibility -DXPR Theme (dxt) owns the visual baseline: heading weight, text colors, backgrounds, font sizes. Content markup (dxb) inherits these automatically. Configure the theme FIRST — don't compensate with utility classes in content. +DXPR Theme (dxt) owns the visual baseline: heading weight, text colors, backgrounds, font sizes. Content markup (dxb) inherits these automatically. Configure the theme FIRST; don't compensate with utility classes in content. ## Workflow diff --git a/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml index 089a4844e..1ea590d20 100644 --- a/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml +++ b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} third_party_settings: language_hierarchy: fallback_langcode: nl diff --git a/recipes/easy_email_text_format/config/editor.editor.easy_email.yml b/recipes/easy_email_text_format/config/editor.editor.easy_email.yml index e31cc0a14..b1a9371f4 100644 --- a/recipes/easy_email_text_format/config/editor.editor.easy_email.yml +++ b/recipes/easy_email_text_format/config/editor.editor.easy_email.yml @@ -60,7 +60,7 @@ settings: styles: true multiBlock: true ckeditor5_sourceEditing: - allowed_tags: { } + allowed_tags: {} image_upload: status: true scheme: public diff --git a/recipes/easy_email_text_format/config/filter.format.easy_email.yml b/recipes/easy_email_text_format/config/filter.format.easy_email.yml index 0d23bbc16..e8f40c0b5 100644 --- a/recipes/easy_email_text_format/config/filter.format.easy_email.yml +++ b/recipes/easy_email_text_format/config/filter.format.easy_email.yml @@ -12,25 +12,25 @@ filters: provider: editor status: true weight: -45 - settings: { } + settings: {} filter_align: id: filter_align provider: filter status: true weight: -48 - settings: { } + settings: {} filter_autop: id: filter_autop provider: filter status: false weight: -44 - settings: { } + settings: {} filter_caption: id: filter_caption provider: filter status: true weight: -47 - settings: { } + settings: {} filter_html: id: filter_html provider: filter @@ -45,25 +45,25 @@ filters: provider: filter status: false weight: -49 - settings: { } + settings: {} filter_html_image_secure: id: filter_html_image_secure provider: filter status: false weight: -42 - settings: { } + settings: {} filter_htmlcorrector: id: filter_htmlcorrector provider: filter status: true weight: -46 - settings: { } + settings: {} filter_image_lazy_load: id: filter_image_lazy_load provider: filter status: true weight: -41 - settings: { } + settings: {} filter_url: id: filter_url provider: filter diff --git a/recipes/easy_email_types_core/config/core.entity_form_display.easy_email.user_created_approval_admin.default.yml b/recipes/easy_email_types_core/config/core.entity_form_display.easy_email.user_created_approval_admin.default.yml index e977e7a68..f8798acef 100644 --- a/recipes/easy_email_types_core/config/core.entity_form_display.easy_email.user_created_approval_admin.default.yml +++ b/recipes/easy_email_types_core/config/core.entity_form_display.easy_email.user_created_approval_admin.default.yml @@ -18,7 +18,7 @@ content: region: content settings: progress_indicator: throbber - third_party_settings: { } + third_party_settings: {} attachment_path: type: string_textfield weight: -4 @@ -26,7 +26,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} bcc_address: type: string_textfield weight: -4 @@ -34,7 +34,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} bcc_uid: type: entity_reference_autocomplete weight: 5 @@ -44,7 +44,7 @@ content: match_limit: 10 size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} body_html: type: text_textarea weight: 0 @@ -52,7 +52,7 @@ content: settings: rows: 5 placeholder: '' - third_party_settings: { } + third_party_settings: {} body_plain: type: string_textarea weight: 0 @@ -60,7 +60,7 @@ content: settings: rows: 5 placeholder: '' - third_party_settings: { } + third_party_settings: {} cc_address: type: string_textfield weight: -4 @@ -68,7 +68,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} cc_uid: type: entity_reference_autocomplete weight: 5 @@ -78,7 +78,7 @@ content: match_limit: 10 size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} creator_uid: type: entity_reference_autocomplete weight: 5 @@ -88,7 +88,7 @@ content: match_limit: 10 size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} field_related_user: type: entity_reference_autocomplete weight: 26 @@ -98,7 +98,7 @@ content: match_limit: 10 size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} from_address: type: string_textfield weight: -4 @@ -106,7 +106,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} from_name: type: string_textfield weight: -4 @@ -114,7 +114,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} inbox_preview: type: string_textarea weight: 0 @@ -122,7 +122,7 @@ content: settings: rows: 5 placeholder: '' - third_party_settings: { } + third_party_settings: {} recipient_address: type: string_textfield weight: -4 @@ -130,7 +130,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} recipient_uid: type: entity_reference_autocomplete weight: 5 @@ -140,7 +140,7 @@ content: match_limit: 10 size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} reply_to: type: string_textfield weight: -4 @@ -148,7 +148,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} subject: type: string_textfield weight: -4 @@ -156,7 +156,7 @@ content: settings: size: 60 placeholder: '' - third_party_settings: { } + third_party_settings: {} hidden: key: true label: true diff --git a/recipes/easy_email_types_core/config/core.entity_view_display.easy_email.user_created_approval_admin.default.yml b/recipes/easy_email_types_core/config/core.entity_view_display.easy_email.user_created_approval_admin.default.yml index 301d5383d..d439fad61 100644 --- a/recipes/easy_email_types_core/config/core.entity_view_display.easy_email.user_created_approval_admin.default.yml +++ b/recipes/easy_email_types_core/config/core.entity_view_display.easy_email.user_created_approval_admin.default.yml @@ -18,7 +18,7 @@ content: label: hidden settings: use_description_as_link_text: true - third_party_settings: { } + third_party_settings: {} weight: 0 region: content attachment_path: @@ -26,7 +26,7 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content bcc_address: @@ -34,28 +34,28 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content bcc_uid: type: author label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content body_html: type: text_default label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content body_plain: type: basic_string label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content cc_address: @@ -63,21 +63,21 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content cc_uid: type: author label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content creator_uid: type: author label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content field_related_user: @@ -85,7 +85,7 @@ content: label: above settings: link: true - third_party_settings: { } + third_party_settings: {} weight: 1 region: content from_address: @@ -93,7 +93,7 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content from_name: @@ -101,14 +101,14 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content inbox_preview: type: basic_string label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content key: @@ -116,7 +116,7 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content label: @@ -124,7 +124,7 @@ content: label: hidden settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -5 region: content recipient_address: @@ -132,14 +132,14 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content recipient_uid: type: author label: hidden - settings: { } - third_party_settings: { } + settings: {} + third_party_settings: {} weight: 0 region: content reply_to: @@ -147,7 +147,7 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content subject: @@ -155,7 +155,7 @@ content: label: above settings: link_to_entity: false - third_party_settings: { } + third_party_settings: {} weight: -4 region: content -hidden: { } +hidden: {} diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_activation.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_activation.yml index 827c7d1b2..7e0c903eb 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_activation.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_activation.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_activation label: 'User: Account Activation' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nYour account at [site:name] has been activated.\r\n\r\nYou may now log in by clicking this link or copying and pasting it into your browser:\r\n\r\n[easy_email:recipient_uid:0:entity:one-time-login-url]\r\n\r\nThis link can only be used once to log in and will lead you to a page where you can set your password.\r\n\r\nAfter setting your password, you will be able to log in at [site:login-url] in the future using:\r\n\r\nusername: [user:account-name]\r\npassword: Your password\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_blocked.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_blocked.yml index 620078faf..80270d4bf 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_blocked.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_blocked.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_blocked label: 'User: Account Blocked' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nYour account on [site:name] has been blocked.\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_canceled.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_canceled.yml index 67319a82e..9c3cd6d60 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_canceled.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_canceled.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_canceled label: 'User: Account Canceled' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nYour account on [site:name] has been canceled.\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_cancellation_confirmation.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_cancellation_confirmation.yml index 6a560b7fa..b515a8fdc 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_cancellation_confirmation.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_cancellation_confirmation.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_cancellation_confirmation label: 'User: Account Cancellation Confirmation' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name]\r\n\r\nA request to cancel your account has been made at [site:name].\r\n\r\nYou may now cancel your account on [site:url-brief] by clicking this link or copying and pasting it into your browser:\r\n\r\n[easy_email:recipient_uid:0:entity:cancel-url]\r\n\r\nNOTE: The cancellation of your account is not reversible.\r\n\r\nThis link expires in one day and nothing will happen if it is not used.\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_approval_admin.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_approval_admin.yml index e6b68cfb6..cbd2222cd 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_approval_admin.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_approval_admin.yml @@ -1,13 +1,13 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_approval_admin label: 'User: Created - Awaiting Approval - Admin Notification' key: '' recipient: - '[site:mail]' -cc: { } -bcc: { } +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -18,7 +18,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:field_related_user:0:entity:display-name] has applied for an account.\r\n\r\n[easy_email:field_related_user:0:entity:edit-url]" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_awaiting_approval.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_awaiting_approval.yml index 3ffa59b3b..88a688a3c 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_awaiting_approval.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_awaiting_approval.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_awaiting_approval label: 'User: Created - Awaiting Approval' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nThank you for registering at [site:name]. Your application for an account is currently pending approval. Once it has been approved, you will receive another email containing information about how to log in, set your password, and other details.\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_by_admin.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_by_admin.yml index e0b22583a..33db53a9b 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_by_admin.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_by_admin.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_by_admin label: 'User: Created - By Administrator' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nA site administrator at [site:name] has created an account for you. You may now log in by clicking this link or copying and pasting it into your browser:\r\n\r\n[easy_email:recipient_uid:0:entity:one-time-login-url]\r\n\r\nThis link can only be used once to log in and will lead you to a page where you can set your password.\r\n\r\nAfter setting your password, you will be able to log in at [site:login-url] in the future using:\r\n\r\nusername: [easy_email:recipient_uid:0:entity:account-name]\r\npassword: Your password\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_no_approval.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_no_approval.yml index 383bb87ff..551b69433 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_no_approval.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_created_no_approval.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_no_approval label: 'User: Created - No Approval Required' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nThank you for registering at [site:name]. You may now log in by clicking this link or copying and pasting it into your browser:\r\n\r\n[easy_email:recipient_uid:0:entity:one-time-login-url]\r\n\r\nThis link can only be used once to log in and will lead you to a page where you can set your password.\r\n\r\nAfter setting your password, you will be able to log in at [site:login-url] in the future using:\r\n\r\nusername: [easy_email:recipient_uid:0:entity:name]\r\npassword: Your password\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_password_recovery.yml b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_password_recovery.yml index 1e20fc12b..cca60cc53 100644 --- a/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_password_recovery.yml +++ b/recipes/easy_email_types_core/config/easy_email.easy_email_type.user_password_recovery.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_password_recovery label: 'User: Password Recovery' key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: "[easy_email:recipient_uid:0:entity:display-name],\r\n\r\nA request to reset the password for your account has been made at [site:name].\r\n\r\nYou may now log in by clicking this link or copying and pasting it into your browser:\r\n\r\n[easy_email:recipient_uid:0:entity:one-time-login-url]\r\n\r\nThis link can only be used once to log in and will lead you to a page where you can set your password. It expires after one day and nothing will happen if it's not used.\r\n\r\n-- [site:name] team" generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: public attachmentDirectory: '' diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_activation.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_activation.yml index 7529d6131..52a29e0b7 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_activation.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_activation.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_activation label: 'User: Account Activation' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_blocked.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_blocked.yml index e5734fb12..25c571a16 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_blocked.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_blocked.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_blocked label: 'User: Account Blocked' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_canceled.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_canceled.yml index 1de274f40..519c5dc9d 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_canceled.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_canceled.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_canceled label: 'User: Account Canceled' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_cancellation_confirmation.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_cancellation_confirmation.yml index 36cffffdf..2a9f0bc01 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_cancellation_confirmation.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_cancellation_confirmation.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_cancellation_confirmation label: 'User: Account Cancellation Confirmation' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_approval_admin.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_approval_admin.yml index 51537195c..d449a149d 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_approval_admin.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_approval_admin.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_approval_admin label: 'User: Created - Awaiting Approval - Admin Notification' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_awaiting_approval.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_awaiting_approval.yml index fc9f3faf7..8cafe575c 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_awaiting_approval.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_awaiting_approval.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_awaiting_approval label: 'User: Created - Awaiting Approval' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_by_admin.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_by_admin.yml index 3e60144a3..bfc58c99a 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_by_admin.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_by_admin.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_by_admin label: 'User: Created - By Administrator' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_no_approval.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_no_approval.yml index 99a21032f..340057702 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_no_approval.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_created_no_approval.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_created_no_approval label: 'User: Created - No Approval Required' module: user diff --git a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_password_recovery.yml b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_password_recovery.yml index 952f08619..bdc5299d2 100644 --- a/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_password_recovery.yml +++ b/recipes/easy_email_types_core/config/easy_email_override.easy_email_override.user_password_recovery.yml @@ -1,6 +1,6 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: user_password_recovery label: 'User: Password Recovery' module: user diff --git a/recipes/easy_email_types_core/config/field.field.easy_email.user_created_approval_admin.field_related_user.yml b/recipes/easy_email_types_core/config/field.field.easy_email.user_created_approval_admin.field_related_user.yml index 42f8b18a2..2d07a48d0 100644 --- a/recipes/easy_email_types_core/config/field.field.easy_email.user_created_approval_admin.field_related_user.yml +++ b/recipes/easy_email_types_core/config/field.field.easy_email.user_created_approval_admin.field_related_user.yml @@ -12,7 +12,7 @@ label: 'Related User' description: '' required: false translatable: false -default_value: { } +default_value: {} default_value_callback: '' settings: handler: 'default:user' diff --git a/recipes/easy_email_types_core/config/field.storage.easy_email.field_related_user.yml b/recipes/easy_email_types_core/config/field.storage.easy_email.field_related_user.yml index 52dad7bd8..8ac37a9bf 100644 --- a/recipes/easy_email_types_core/config/field.storage.easy_email.field_related_user.yml +++ b/recipes/easy_email_types_core/config/field.storage.easy_email.field_related_user.yml @@ -14,6 +14,6 @@ module: core locked: false cardinality: 1 translatable: true -indexes: { } +indexes: {} persist_with_no_fields: false custom_storage: false diff --git a/recipes/easy_email_types_default/config/easy_email.easy_email_type.default.yml b/recipes/easy_email_types_default/config/easy_email.easy_email_type.default.yml index 13cbe6b56..19e3341f2 100644 --- a/recipes/easy_email_types_default/config/easy_email.easy_email_type.default.yml +++ b/recipes/easy_email_types_default/config/easy_email.easy_email_type.default.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: default label: Default key: '' -recipient: { } -cc: { } -bcc: { } +recipient: {} +cc: {} +bcc: {} fromName: '' fromAddress: '' replyToAddress: '' @@ -17,7 +17,7 @@ bodyHtml: format: easy_email bodyPlain: '' generateBodyPlain: false -attachment: { } +attachment: {} saveAttachment: false attachmentScheme: private attachmentDirectory: '' diff --git a/recipes/easy_email_types_default/config/easy_email_override.easy_email_override.default.yml b/recipes/easy_email_types_default/config/easy_email_override.easy_email_override.default.yml index f1ed21792..e1e6ed4e2 100644 --- a/recipes/easy_email_types_default/config/easy_email_override.easy_email_override.default.yml +++ b/recipes/easy_email_types_default/config/easy_email_override.easy_email_override.default.yml @@ -1,12 +1,12 @@ langcode: en status: true -dependencies: { } +dependencies: {} id: default label: Default module: '*' key: '*' easy_email_type: default -param_map: { } +param_map: {} copied_fields: from: true reply_to: true From 18f9b6683a3d74f864fdce00f895aad0d83e26a0 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:27:23 +0200 Subject: [PATCH 4/4] fix: address review feedback on fallback chain safety and view order 1. nl-be fallback now checks if nl is actually installed before using it; falls back to the site default language when nl is absent. When nl is added later, nl-be is updated automatically. 2. Added hook_view_insert() so views imported after module install (e.g. from recipes applied in a different order) also get the language_hierarchy fallback filter swap. 3. Removed hardcoded third_party_settings from language.entity.nl-be.yml; the hook handles fallback configuration dynamically. 4. Consolidated duplicated filter-swap logic into a shared helper in the .module file, called by both hook_install and hook_view_insert. --- .../config/language.entity.nl-be.yml | 3 - .../dxpr_cms_multilingual_content.install | 85 ++-------------- .../dxpr_cms_multilingual_content.module | 96 ++++++++++++++++++- 3 files changed, 99 insertions(+), 85 deletions(-) diff --git a/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml index 1ea590d20..7462f0634 100644 --- a/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml +++ b/recipes/dxpr_cms_multilingual/config/language.entity.nl-be.yml @@ -1,9 +1,6 @@ langcode: en status: true dependencies: {} -third_party_settings: - language_hierarchy: - fallback_langcode: nl id: nl-be label: Vlaams direction: ltr diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install index c2ed01ef7..b4bb4c026 100644 --- a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.install @@ -10,99 +10,30 @@ declare(strict_types=1); /** * Implements hook_install(). * - * Swaps standard language filters in Views with the language_hierarchy - * fallback filter so that untranslated content falls back through - * the configured language chain instead of showing empty listings. + * Configures language_hierarchy fallback chains for all installed languages + * and swaps standard language filters in Views with the language_hierarchy + * fallback filter. Languages and views created after install are handled by + * hook_configurable_language_insert() and hook_view_insert() in the .module. */ function dxpr_cms_multilingual_content_install(): void { if (!\Drupal::moduleHandler()->moduleExists('language_hierarchy')) { return; } - _dxpr_cms_multilingual_content_configure_fallback_chains(); - _dxpr_cms_multilingual_content_swap_views_filters(); -} - -/** - * Configures language_hierarchy fallback chains for all installed languages. - */ -function _dxpr_cms_multilingual_content_configure_fallback_chains(): void { - $overrides = [ - 'nl-be' => 'nl', - ]; $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); - $languages = \Drupal::entityTypeManager() ->getStorage('configurable_language') ->loadMultiple(); foreach ($languages as $language) { - $langcode = $language->id(); - if ($langcode === $default_langcode) { + if ($language->id() === $default_langcode) { continue; } - $fallback = $overrides[$langcode] ?? $default_langcode; - $language->setThirdPartySetting('language_hierarchy', 'fallback_langcode', $fallback); - $language->save(); + _dxpr_cms_multilingual_content_set_language_fallback($language); } -} - -/** - * Swaps standard language filters in Views with language_hierarchy fallback. - */ -function _dxpr_cms_multilingual_content_swap_views_filters(): void { - $view_storage = \Drupal::entityTypeManager()->getStorage('view'); - $views = $view_storage->loadMultiple(); + $views = \Drupal::entityTypeManager()->getStorage('view')->loadMultiple(); foreach ($views as $view) { - $display = $view->get('display'); - $changed = FALSE; - - foreach ($display as $display_id => &$display_config) { - $filters = $display_config['display_options']['filters'] ?? []; - - foreach ($filters as $filter_id => $filter) { - if (($filter['plugin_id'] ?? '') !== 'language' - || ($filter['entity_field'] ?? '') !== 'langcode') { - continue; - } - - $value = $filter['value'] ?? []; - $is_interface = is_array($value) - && isset($value['***LANGUAGE_language_interface***']); - - if (!$is_interface) { - continue; - } - - unset($display_config['display_options']['filters'][$filter_id]); - - $display_config['display_options']['filters']['language_hierarchy_content_language_fallback_limited_langcode'] = [ - 'id' => 'language_hierarchy_content_language_fallback_limited_langcode', - 'table' => $filter['table'], - 'field' => 'language_hierarchy_content_language_fallback_limited_langcode', - 'relationship' => 'none', - 'group_type' => 'group', - 'admin_label' => '', - 'plugin_id' => 'language_hierarchy_content_language_fallback_limited', - 'value' => '1', - 'group' => $filter['group'] ?? 1, - 'exposed' => FALSE, - 'is_grouped' => FALSE, - ]; - $changed = TRUE; - } - } - unset($display_config); - - if ($changed) { - $deps = $view->get('dependencies'); - if (!in_array('language_hierarchy', $deps['module'] ?? [], TRUE)) { - $deps['module'][] = 'language_hierarchy'; - $view->set('dependencies', $deps); - } - $view->set('display', $display); - $view->save(); - } + _dxpr_cms_multilingual_content_swap_view_language_filter($view); } } diff --git a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module index bf5fe335e..9c0fba68e 100644 --- a/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module +++ b/web/modules/custom/dxpr_cms_multilingual_content/dxpr_cms_multilingual_content.module @@ -9,17 +9,25 @@ declare(strict_types=1); use Drupal\language\ConfigurableLanguageInterface; use Drupal\dxpr_cms_multilingual_content\EventSubscriber\TranslationMergeSubscriber; +use Drupal\views\ViewEntityInterface; /** * Implements hook_ENTITY_TYPE_insert() for configurable_language. - * - * When a new language is added (e.g. during multilingual install), scan the - * multilingual recipe content and add translations for that language to any - * existing entities. */ function dxpr_cms_multilingual_content_configurable_language_insert(ConfigurableLanguageInterface $language): void { if (\Drupal::moduleHandler()->moduleExists('language_hierarchy')) { _dxpr_cms_multilingual_content_set_language_fallback($language); + + // When Dutch is added, update nl-be to fall back to nl instead of en. + if ($language->id() === 'nl') { + $nlBe = \Drupal::entityTypeManager() + ->getStorage('configurable_language') + ->load('nl-be'); + if ($nlBe) { + $nlBe->setThirdPartySetting('language_hierarchy', 'fallback_langcode', 'nl'); + $nlBe->save(); + } + } } try { @@ -39,8 +47,24 @@ function dxpr_cms_multilingual_content_configurable_language_insert(Configurable } } +/** + * Implements hook_ENTITY_TYPE_insert() for view. + * + * Swaps standard language filters with language_hierarchy fallback filters + * on views imported after module install (e.g. from later recipe application). + */ +function dxpr_cms_multilingual_content_view_insert(ViewEntityInterface $view): void { + if (!\Drupal::moduleHandler()->moduleExists('language_hierarchy')) { + return; + } + _dxpr_cms_multilingual_content_swap_view_language_filter($view); +} + /** * Sets the language_hierarchy fallback for a language entity. + * + * Regional variants (e.g. nl-be) fall back to their parent language if it is + * installed, otherwise to the site default language. */ function _dxpr_cms_multilingual_content_set_language_fallback(ConfigurableLanguageInterface $language): void { $overrides = [ @@ -53,7 +77,69 @@ function _dxpr_cms_multilingual_content_set_language_fallback(ConfigurableLangua return; } - $fallback = $overrides[$langcode] ?? $default_langcode; + $fallback = $default_langcode; + if (isset($overrides[$langcode])) { + $target = $overrides[$langcode]; + $exists = \Drupal::entityTypeManager() + ->getStorage('configurable_language') + ->load($target); + if ($exists) { + $fallback = $target; + } + } + $language->setThirdPartySetting('language_hierarchy', 'fallback_langcode', $fallback); $language->save(); } + +/** + * Swaps the standard language interface filter with language_hierarchy fallback. + */ +function _dxpr_cms_multilingual_content_swap_view_language_filter(ViewEntityInterface $view): void { + $display = $view->get('display'); + $changed = FALSE; + + foreach ($display as $display_id => &$display_config) { + $filters = $display_config['display_options']['filters'] ?? []; + + foreach ($filters as $filter_id => $filter) { + if (($filter['plugin_id'] ?? '') !== 'language' + || ($filter['entity_field'] ?? '') !== 'langcode') { + continue; + } + + $value = $filter['value'] ?? []; + if (!is_array($value) || !isset($value['***LANGUAGE_language_interface***'])) { + continue; + } + + unset($display_config['display_options']['filters'][$filter_id]); + + $display_config['display_options']['filters']['language_hierarchy_content_language_fallback_limited_langcode'] = [ + 'id' => 'language_hierarchy_content_language_fallback_limited_langcode', + 'table' => $filter['table'], + 'field' => 'language_hierarchy_content_language_fallback_limited_langcode', + 'relationship' => 'none', + 'group_type' => 'group', + 'admin_label' => '', + 'plugin_id' => 'language_hierarchy_content_language_fallback_limited', + 'value' => '1', + 'group' => $filter['group'] ?? 1, + 'exposed' => FALSE, + 'is_grouped' => FALSE, + ]; + $changed = TRUE; + } + } + unset($display_config); + + if ($changed) { + $deps = $view->get('dependencies'); + if (!in_array('language_hierarchy', $deps['module'] ?? [], TRUE)) { + $deps['module'][] = 'language_hierarchy'; + $view->set('dependencies', $deps); + } + $view->set('display', $display); + $view->save(); + } +}