From e1fc76a1d2d9ff925bdbb7d4440f15911c03c915 Mon Sep 17 00:00:00 2001
From: Lauri Eskola <lauri.eskola@acquia.com>
Date: Thu, 28 Sep 2023 12:00:06 +0300
Subject: [PATCH] Issue #3273986 by ifrik, xurizaemon, rpayanm, Wim Leers,
 Charles Belov, jonathan_hunt: Third option for the CKEditor 5 "Language"
 button: site_configured (in addition to un and all)

---
 .../config/schema/ckeditor5.schema.yml        |  6 +-
 .../src/Plugin/CKEditor5Plugin/Language.php   | 98 ++++++++++++++++---
 .../FunctionalJavascript/CKEditor5Test.php    | 86 +++++++++++-----
 .../tests/src/Unit/LanguagePluginTest.php     | 43 +++++++-
 4 files changed, 187 insertions(+), 46 deletions(-)

diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
index 20c31399c4af..52a9e1afbddf 100644
--- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
+++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
@@ -41,10 +41,14 @@ ckeditor5.plugin.ckeditor5_language:
         # Configuring this does not make sense without the corresponding button.
         CKEditor5ToolbarItemDependencyConstraint:
           toolbarItem: textPartLanguage
-        # Only two possible values are accepted.
+        # Only the following values are accepted.
         Choice:
+          # United Nations "official languages".
           - un
+          # Drupal's predefined language list.
           - all
+          # Languages configured at /admin/config/regional/language for the site.
+          - site_configured
 
 # Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading
 ckeditor5.plugin.ckeditor5_heading:
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php
index ccbeb9884f91..3e0ac2a968aa 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php
@@ -4,13 +4,19 @@
 
 namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;
 
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
 use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
 use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
-use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageManager;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Core\Url;
 use Drupal\editor\EditorInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * CKEditor 5 Language plugin.
@@ -18,23 +24,72 @@
  * @internal
  *   Plugin classes are internal.
  */
-class Language extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
+class Language extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, ContainerFactoryPluginInterface {
 
   use CKEditor5PluginConfigurableTrait;
 
+  /**
+   * Language constructor.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
+   *   The language manager.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $routeProvider
+   *   The route provider.
+   */
+  public function __construct(array $configuration, string $plugin_id, CKEditor5PluginDefinition $plugin_definition, protected LanguageManagerInterface $languageManager, protected RouteProviderInterface $routeProvider) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('language_manager'),
+      $container->get('router.route_provider'),
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
   public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
-    $predefined_languages = $this->configuration['language_list'] === 'all' ?
-      LanguageManager::getStandardLanguageList() :
-      LanguageManager::getUnitedNationsLanguageList();
+    $languages = NULL;
+    switch ($this->configuration['language_list']) {
+      case 'site_configured':
+        $configured_languages = $this->languageManager->getLanguages();
+        $languages = [];
+        foreach ($configured_languages as $language) {
+          $languages[$language->getId()] = [
+            $language->getName(),
+            '',
+            $language->getDirection(),
+          ];
+        }
+        break;
+
+      case 'all':
+        $languages = LanguageManager::getStandardLanguageList();
+        break;
+
+      case 'un':
+        $languages = LanguageManager::getUnitedNationsLanguageList();
+    }
 
     // Generate the language_list setting as expected by the CKEditor Language
     // plugin, but key the values by the full language name so that we can sort
     // them later on.
     $language_list = [];
-    foreach ($predefined_languages as $langcode => $language) {
+    foreach ($languages as $langcode => $language) {
       $english_name = $language[0];
       $direction = empty($language[2]) ? NULL : $language[2];
       $language_list[$english_name] = [
@@ -60,20 +115,35 @@ public function getDynamicPluginConfig(array $static_plugin_config, EditorInterf
    * @see editor_image_upload_settings_form()
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
-    $predefined_languages = LanguageManager::getStandardLanguageList();
+    $configured = count($this->languageManager->getLanguages());
+    $predefined = count(LanguageManager::getStandardLanguageList());
+    $united_nations = count(LanguageManager::getUnitedNationsLanguageList());
+
+    $language_list_description_args = [
+      ':united-nations-official' => 'https://www.un.org/en/sections/about-un/official-languages',
+      '@count_predefined' => $predefined,
+      '@count_united_nations' => $united_nations,
+      '@count_configured' => $configured,
+    ];
+    // If Language is enabled, link to the configuration route.
+    if ($this->routeProvider->getRoutesByNames(['entity.configurable_language.collection'])) {
+      $language_list_description = $this->t('The list of languages in the CKEditor "Language" dropdown can present the <a href=":united-nations-official">@count_united_nations official languages of the UN</a>, all @count_predefined languages predefined in Drupal, or the <a href=":admin-configure-languages">@count_configured languages configured for this site</a>.', $language_list_description_args + [':admin-configure-languages' => Url::fromRoute('entity.configurable_language.collection')->toString()]);
+    }
+    else {
+      $language_list_description = $this->t('The list of languages in the CKEditor "Language" dropdown can present the <a href=":united-nations-official">@count_united_nations official languages of the UN</a>, all @count_predefined languages predefined in Drupal, or the languages configured for this site.', $language_list_description_args);
+    }
+
     $form['language_list'] = [
       '#title' => $this->t('Language list'),
       '#title_display' => 'invisible',
       '#type' => 'select',
       '#options' => [
-        'un' => $this->t("United Nations' official languages"),
-        'all' => $this->t('All @count languages', ['@count' => count($predefined_languages)]),
+        'un' => $this->t("United Nations' official languages (@count)", ['@count' => $united_nations]),
+        'all' => $this->t('Drupal predefined languages (@count)', ['@count' => $predefined]),
+        'site_configured' => $this->t("Site-configured languages (@count)", ['@count' => $configured]),
       ],
       '#default_value' => $this->configuration['language_list'],
-      '#description' => $this->t('The list of languages to show in the language dropdown. The basic list will only show the <a href=":url">six official languages of the UN</a>. The extended list will show all @count languages that are available in Drupal.', [
-        ':url' => 'https://www.un.org/en/sections/about-un/official-languages',
-        '@count' => count($predefined_languages),
-      ]),
+      '#description' => $language_list_description,
     ];
 
     return $form;
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
index 61b337f1fb6a..aa85bc1766c1 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
@@ -7,13 +7,14 @@
 use Drupal\editor\Entity\Editor;
 use Drupal\file\Entity\File;
 use Drupal\filter\Entity\FilterFormat;
+use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\node\Entity\Node;
 use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
 use Drupal\Tests\TestFileCreationTrait;
 use Drupal\user\RoleInterface;
 use Symfony\Component\Validator\ConstraintViolation;
 
-// cspell:ignore esque splitbutton upcasted sourceediting
+// cspell:ignore esque māori sourceediting splitbutton upcasted
 
 /**
  * Tests for CKEditor 5.
@@ -31,6 +32,7 @@ class CKEditor5Test extends CKEditor5TestBase {
    */
   protected static $modules = [
     'media_library',
+    'language',
   ];
 
   /**
@@ -41,7 +43,7 @@ public function testExistingContent() {
     $assert_session = $this->assertSession();
 
     // Add a node with text rendered via the Plain Text format.
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $page->fillField('title[0][value]', 'My test content');
     $page->fillField('body[0][value]', '<p>This is test content</p>');
     $page->pressButton('Save');
@@ -103,7 +105,7 @@ function (ConstraintViolation $v) {
       ))
     ));
 
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $this->waitForEditor();
     $page->fillField('title[0][value]', 'My test content');
 
@@ -147,7 +149,7 @@ public function testHeadingsPlugin() {
     $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
     $this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>');
 
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button'));
 
     $page->find('css', '.ck-heading-dropdown button')->click();
@@ -188,7 +190,7 @@ public function testHeadingsPlugin() {
 
     $page->pressButton('Save configuration');
 
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button'));
 
     $page->find('css', '.ck-heading-dropdown button')->click();
@@ -212,12 +214,43 @@ public function testHeadingsPlugin() {
   }
 
   /**
-   * Test for plugin Language of parts.
+   * Test for Language of Parts plugin.
    */
   public function testLanguageOfPartsPlugin() {
     $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
 
+    $this->languageOfPartsPluginInitialConfigurationHelper($page, $assert_session);
+
+    // Test for "United Nations' official languages" option.
+    $languages = LanguageManager::getUnitedNationsLanguageList();
+    $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'un');
+    $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
+
+    // Test for "Drupal predefined languages" option.
+    $languages = LanguageManager::getStandardLanguageList();
+    $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'all');
+    $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
+
+    // Test for "Site-configured languages" option.
+    ConfigurableLanguage::createFromLangcode('ar')->save();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+    ConfigurableLanguage::createFromLangcode('mi')->setName('Māori')->save();
+    $configured_languages = \Drupal::languageManager()->getLanguages();
+    $languages = [];
+    foreach ($configured_languages as $language) {
+      $language_name = $language->getName();
+      $language_code = $language->getId();
+      $languages[$language_code] = [$language_name];
+    }
+    $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'site_configured');
+    $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
+  }
+
+  /**
+   * Helper to configure CKEditor5 with Language plugin.
+   */
+  public function languageOfPartsPluginInitialConfigurationHelper($page, $assert_session) {
     $this->createNewTextFormat($page, $assert_session);
     // Press arrow down key to add the button to the active toolbar.
     $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-textPartLanguage'));
@@ -248,21 +281,15 @@ public function testLanguageOfPartsPlugin() {
 
     // Confirm there are no longer any warnings.
     $assert_session->waitForElementRemoved('css', '[data-drupal-messages] [role="alert"]');
-
-    // Test for "United Nations' official languages" option.
-    $languages = LanguageManager::getUnitedNationsLanguageList();
-    $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages, "un");
-
-    // Test for "All 95 languages" option.
-    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
-    $languages = LanguageManager::getStandardLanguageList();
-    $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages, "all");
+    $page->pressButton('Save configuration');
+    $assert_session->responseContains('Added text format <em class="placeholder">ckeditor5</em>.');
   }
 
   /**
-   * Validate the available languages on the basis of selected language option.
+   * Helper to set language list option for CKEditor.
    */
-  public function languageOfPartsPluginTestHelper($page, $assert_session, $predefined_languages, $option) {
+  public function languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, $option) {
+    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
     $this->assertNotEmpty($assert_session->waitForElement('css', 'a[href^="#edit-editor-settings-plugins-ckeditor5-language"]'));
 
     // Set correct value.
@@ -271,9 +298,14 @@ public function languageOfPartsPluginTestHelper($page, $assert_session, $predefi
     $page->selectFieldOption('editor[settings][plugins][ckeditor5_language][language_list]', $option);
     $assert_session->assertWaitOnAjaxRequest();
     $page->pressButton('Save configuration');
+    $assert_session->responseContains('The text format <em class="placeholder">ckeditor5</em> has been updated.');
+  }
 
-    // Validate plugin on node add page.
-    $this->drupalGet('node/add');
+  /**
+   * Validate expected languages available in editor.
+   */
+  public function languageOfPartsPluginTestHelper($page, $assert_session, $configured_languages) {
+    $this->drupalGet('node/add/page');
     $this->assertNotEmpty($assert_session->waitForText('Choose language'));
 
     // Click on the dropdown button.
@@ -290,13 +322,13 @@ public function languageOfPartsPluginTestHelper($page, $assert_session, $predefi
     foreach ($current_languages as $item) {
       $languages[] = $item->getText();
     }
+
     // Return the values from a single column.
-    $predefined_languages = array_column($predefined_languages, 0);
+    $configured_languages = array_column($configured_languages, 0);
 
     // Sort on full language name.
-    asort($predefined_languages);
-
-    $this->assertSame(array_values($predefined_languages), $languages);
+    asort($configured_languages);
+    $this->assertSame(array_values($configured_languages), $languages);
   }
 
   /**
@@ -518,7 +550,7 @@ public function testEditorFileReferenceIntegration() {
     $assert_session->assertWaitOnAjaxRequest();
     $this->saveNewTextFormat($page, $assert_session);
 
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $page->fillField('title[0][value]', 'My test content');
 
     // Ensure that CKEditor 5 is focused.
@@ -566,7 +598,7 @@ public function testEmphasis() {
     $assert_session = $this->assertSession();
 
     // Add a node with text rendered via the Plain Text format.
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $page->fillField('title[0][value]', 'My test content');
     $page->fillField('body[0][value]', '<p>This is a <em>test!</em></p>');
     $page->pressButton('Save');
@@ -624,7 +656,7 @@ function (ConstraintViolation $v) {
     $ordered_list_html = '<ol><li>apple</li><li>banana</li><li>cantaloupe</li></ol>';
     $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $page->fillField('title[0][value]', 'My test content');
     $this->pressEditorButton('Source');
     $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea');
@@ -690,7 +722,7 @@ public function testFilterHtmlAllowedGlobalAttributes(): void {
     $assert_session = $this->assertSession();
 
     // Add a node with text rendered via the Plain Text format.
-    $this->drupalGet('node/add');
+    $this->drupalGet('node/add/page');
     $page->fillField('title[0][value]', 'Multilingual Hello World');
     // cSpell:disable-next-line
     $page->fillField('body[0][value]', '<p dir="ltr" lang="en">Hello World</p><p dir="rtl" lang="ar">مرحبا بالعالم</p>');
diff --git a/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php b/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php
index 395004ecec5d..de73ca706f09 100644
--- a/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php
+++ b/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php
@@ -5,7 +5,11 @@
 namespace Drupal\Tests\ckeditor5\Unit;
 
 use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Language;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Core\Language\Language as LanguageLanguage;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
 use Drupal\editor\EditorInterface;
 use Drupal\Tests\UnitTestCase;
 
@@ -20,7 +24,7 @@ class LanguagePluginTest extends UnitTestCase {
    * Provides a list of configs to test.
    */
   public static function providerGetDynamicPluginConfig(): array {
-    $un_expected_output = [
+    $united_nations_expected_output = [
       'language' => [
         'textPartLanguage' => [
           [
@@ -54,7 +58,25 @@ public static function providerGetDynamicPluginConfig(): array {
     return [
       'un' => [
         ['language_list' => 'un'],
-        $un_expected_output,
+        $united_nations_expected_output,
+      ],
+      'site_configured' => [
+        ['language_list' => 'site_configured'],
+        [
+          'language' => [
+            'textPartLanguage' => [
+              [
+                'title' => 'Arabic',
+                'languageCode' => 'ar',
+                'textDirection' => 'rtl',
+              ],
+              [
+                'title' => 'German',
+                'languageCode' => 'de',
+              ],
+            ],
+          ],
+        ],
       ],
       'all' => [
         ['language_list' => 'all'],
@@ -66,7 +88,7 @@ public static function providerGetDynamicPluginConfig(): array {
       ],
       'default configuration' => [
         [],
-        $un_expected_output,
+        $united_nations_expected_output,
       ],
     ];
   }
@@ -101,7 +123,20 @@ protected static function buildExpectedDynamicConfig(array $language_list) {
    * @dataProvider providerGetDynamicPluginConfig
    */
   public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
-    $plugin = new Language($configuration, 'ckeditor5_language', NULL);
+    $route_provider = $this->prophesize(RouteProviderInterface::class);
+    $language_manager = $this->prophesize(LanguageManagerInterface::class);
+    $language_manager->getLanguages()->willReturn([
+      new LanguageLanguage([
+        'id' => 'de',
+        'name' => 'German',
+      ]),
+      new LanguageLanguage([
+        'id' => 'ar',
+        'name' => 'Arabic',
+        'direction' => 'rtl',
+      ]),
+    ]);
+    $plugin = new Language($configuration, 'ckeditor5_language', new CKEditor5PluginDefinition(['id' => 'IRRELEVANT-FOR-A-UNIT-TEST']), $language_manager->reveal(), $route_provider->reveal());
     $dynamic_config = $plugin->getDynamicPluginConfig([], $this->prophesize(EditorInterface::class)
       ->reveal());
     $this->assertSame($expected_dynamic_config, $dynamic_config);
-- 
GitLab