From 69aa061710a2a1c6c16311cece1073e2c7523f38 Mon Sep 17 00:00:00 2001
From: Henrik Danielsson <h.danielsson@gmail.com>
Date: Sat, 29 Mar 2025 21:56:40 +0100
Subject: [PATCH] Fix CKEditor plugin translations not filtered on AJAX.

---
 .../ckeditor5/src/Hook/Ckeditor5Hooks.php     | 15 +++-
 .../tests/src/Unit/CKEditor5Test.php          | 83 +++++++++++++++++++
 2 files changed, 96 insertions(+), 2 deletions(-)

diff --git a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php
index 85ead6ae51dd..fe93faee5c3d 100644
--- a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php
+++ b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php
@@ -311,13 +311,24 @@ public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageI
     // This file means CKEditor 5 translations are in use on the page.
     // @see locale_js_alter()
     $placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
+    // If either the placeholder script exists, or one of the resulting language
+    // libraries were already loaded, keep filtering the scripts.
+    $filter_scripts = isset($javascript[$placeholder_file]);
+    if (!$filter_scripts) {
+      foreach ($assets->getAlreadyLoadedLibraries() as $library) {
+        if (str_starts_with($library, 'core/ckeditor5.translations.')) {
+          $filter_scripts = TRUE;
+          break;
+        }
+      }
+    }
     // This file is used to get a weight that will make it possible to aggregate
     // all translation files in a single aggregate.
     $ckeditor_dll_file = 'core/assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js';
-    if (isset($javascript[$placeholder_file])) {
+    if ($filter_scripts) {
       // Use the placeholder file weight to set all the translations files
       // weights so they can be aggregated together as expected.
-      $default_weight = $javascript[$placeholder_file]['weight'];
+      $default_weight = $javascript[$placeholder_file]['weight'] ?? 0;
       if (isset($javascript[$ckeditor_dll_file])) {
         $default_weight = $javascript[$ckeditor_dll_file]['weight'];
       }
diff --git a/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php
index 7a202abfc2f7..ebe46f8b8d5b 100644
--- a/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php
+++ b/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php
@@ -4,7 +4,12 @@
 
 namespace Drupal\Tests\ckeditor5\Unit;
 
+use Drupal\ckeditor5\Hook\Ckeditor5Hooks;
 use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\Language;
 use Drupal\Tests\ckeditor5\Traits\PrivateMethodUnitTestTrait;
 use Drupal\Tests\UnitTestCase;
 
@@ -101,4 +106,82 @@ public static function providerPathsToFormNames(): array {
     ];
   }
 
+  /**
+   * Test the js_alter hook alters when expected.
+   *
+   * @covers \Drupal\ckeditor5\Hook\Ckeditor5Hooks::jsAlter
+   */
+  public function testJsAlterHook(): void {
+    $placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
+    $hooks = new Ckeditor5Hooks();
+    $assets = new AttachedAssets();
+    $language = new Language([
+      'id' => 'en',
+      'name' => 'English',
+      'direction' => 'ltr',
+    ]);
+    $original_javascript = [
+      'keep_this' => [
+        'ckeditor5_langcode' => 'en',
+      ],
+      'remove_this' => [
+        'ckeditor5_langcode' => 'sv',
+      ],
+      'keep_this_too' => [],
+    ];
+    $expected_javascript = [
+      'keep_this' => [
+        'ckeditor5_langcode' => 'en',
+        'weight' => 5,
+      ],
+      'keep_this_too' => [],
+    ];
+
+    $container = new ContainerBuilder();
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class);
+    $module_handler->getModule('locale')->willReturn(TRUE);
+    $container->set('module_handler', $module_handler);
+    \Drupal::setContainer($container);
+
+    // First check that it filters when the placeholder script is present.
+    $javascript = $original_javascript + [
+      $placeholder_file => [
+        'weight' => 5,
+      ],
+    ];
+    $hooks->jsAlter($javascript, $assets, $language);
+    $this->assertEquals($expected_javascript, $javascript);
+
+    // Next check it still filters if the placeholder script has already been
+    // loaded and is now excluded from the list, such as an AJAX operation
+    // loading a new format which uses another set of plugins.
+    $assets->setAlreadyLoadedLibraries([
+      'core/ckeditor5.translations.en',
+    ]);
+    $javascript = $original_javascript;
+    $hooks->jsAlter($javascript, $assets, $language);
+    // There was no placeholder to get the weight from.
+    $expected_javascript['keep_this']['weight'] = 0;
+    $this->assertEquals($expected_javascript, $javascript);
+  }
+
+}
+
+namespace Drupal\ckeditor5\Hook;
+
+if (!function_exists('_ckeditor5_get_langcode_mapping')) {
+
+  /**
+   * Mock language mapping between Drupal and CKEditor 5.
+   *
+   * @param string|bool $lang
+   *   The Drupal langcode to match.
+   *
+   * @return array|mixed|string
+   *   The associated CKEditor 5 langcode.
+   */
+  function _ckeditor5_get_langcode_mapping($lang = FALSE) {
+    return $lang ?: [];
+  }
+
 }
-- 
GitLab