diff --git a/core/modules/ckeditor/ckeditor.services.yml b/core/modules/ckeditor/ckeditor.services.yml index 57157899756..7e7d4715ebc 100644 --- a/core/modules/ckeditor/ckeditor.services.yml +++ b/core/modules/ckeditor/ckeditor.services.yml @@ -2,3 +2,10 @@ services: plugin.manager.ckeditor.plugin: class: Drupal\ckeditor\CKEditorPluginManager arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler'] + cache.ckeditor.languages: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory_method: get + factory_service: cache_factory + arguments: [ckeditor] diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php index fb8181ab555..a5c11c67d4a 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php @@ -7,8 +7,11 @@ namespace Drupal\ckeditor\Plugin\Editor; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\ckeditor\CKEditorPluginManager; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\editor\Plugin\EditorBase; use Drupal\editor\Annotation\Editor; use Drupal\Core\Annotation\Translation; @@ -27,6 +30,20 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { + /** + * The module handler to invoke hooks on. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + /** * The CKEditor plugin manager. * @@ -45,17 +62,30 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { * The plugin implementation definition. * @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager * The CKEditor plugin manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke hooks on. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManager $language_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->ckeditorPluginManager = $ckeditor_plugin_manager; + $this->moduleHandler = $module_handler; + $this->languageManager = $language_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { - return new static($configuration, $plugin_id, $plugin_definition, $container->get('plugin.manager.ckeditor.plugin')); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.ckeditor.plugin'), + $container->get('module_handler'), + $container->get('language_manager') + ); } /** @@ -191,8 +221,6 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { * {@inheritdoc} */ public function getJSSettings(EditorEntity $editor) { - $language_interface = language(Language::TYPE_INTERFACE); - $settings = array(); // Get the settings for all enabled plugins, even the internal ones. @@ -202,13 +230,23 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { $settings += $plugin->getConfig($editor); } + // Fall back on English if no matching language code was found. + $display_langcode = 'en'; + + // Map the interface language code to a CKEditor translation. + $ckeditor_langcodes = $this->getLangcodes(); + $language_interface = $this->languageManager->getLanguage(Language::TYPE_INTERFACE); + if (isset($ckeditor_langcodes[$language_interface->id])) { + $display_langcode = $ckeditor_langcodes[$language_interface->id]; + } + // Next, set the most fundamental CKEditor settings. $external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor); $settings += array( 'toolbar' => $this->buildToolbarJSSetting($editor), 'contentsCss' => $this->buildContentsCssJSSetting($editor), 'extraPlugins' => implode(',', array_keys($external_plugin_files)), - 'language' => $language_interface->id, + 'language' => $display_langcode, // Configure CKEditor to not load styles.js. The StylesCombo plugin will // set stylesSet according to the user's settings, if the "Styles" button // is enabled. We cannot get rid of this until CKEditor will stop loading @@ -227,6 +265,52 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { return $settings; } + /** + * Returns a list of language codes supported by CKEditor. + * + * @return array + * An associative array keyed by language codes. + */ + public function getLangcodes() { + // Cache the file system based language list calculation because this would + // be expensive to calculate all the time. The cache is cleared on core + // upgrades which is the only situation the CKEditor file listing should + // change. + $langcode_cache = cache('ckeditor.languages')->get('langcodes'); + if (!empty($langcode_cache)) { + $langcodes = $langcode_cache->data; + } + if (empty($langcodes)) { + $langcodes = array(); + // Collect languages included with CKEditor based on file listing. + $ckeditor_languages = glob(DRUPAL_ROOT . '/core/assets/vendor/ckeditor/lang/*.js'); + foreach ($ckeditor_languages as $language_filename) { + $langcode = basename($language_filename, '.js'); + $langcodes[$langcode] = $langcode; + } + cache('ckeditor.languages')->set('langcodes', $langcodes); + } + + // Get language mapping if available to map to Drupal language codes. + // This is configurable in the user interface and not expensive to get, so + // we don't include it in the cached language list. + $language_mappings = $this->moduleHandler->moduleExists('language') ? language_get_browser_drupal_langcode_mappings() : array(); + foreach ($langcodes as $langcode) { + // If this language code is available in a Drupal mapping, use that to + // compute a possibility for matching from the Drupal langcode to the + // CKEditor langcode. + // e.g. CKEditor uses the langcode 'no' for Norwegian, Drupal uses 'nb'. + // This would then remove the 'no' => 'no' mapping and replace it with + // 'nb' => 'no'. Now Drupal knows which CKEditor translation to load. + if (isset($language_mappings[$langcode]) && !isset($langcodes[$language_mappings[$langcode]])) { + $langcodes[$language_mappings[$langcode]] = $langcode; + unset($langcodes[$langcode]); + } + } + + return $langcodes; + } + /** * {@inheritdoc} */ diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index bdd651dcfc1..c51c95baddb 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -309,6 +309,27 @@ class CKEditorTest extends DrupalUnitTestBase { $this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.'); } + /** + * Tests language list availability in CKEditor. + */ + function testLanguages() { + // Get CKEditor supported language codes and spot-check. + $this->enableModules(array('language')); + config_install_default_config('module', 'language'); + $langcodes = $this->ckeditor->getLangcodes(); + + // Language codes transformed with browser mappings. + $this->assertTrue($langcodes['pt-pt'] == 'pt', '"pt" properly resolved'); + $this->assertTrue($langcodes['zh-hans'] == 'zh-cn', '"zh-hans" properly resolved'); + + // Language code both in Drupal and CKEditor. + $this->assertTrue($langcodes['gl'] == 'gl', '"gl" properly resolved'); + + // Language codes only in CKEditor. + $this->assertTrue($langcodes['en-au'] == 'en-au', '"en-au" properly resolved'); + $this->assertTrue($langcodes['sr-latn'] == 'sr-latn', '"sr-latn" properly resolved'); + } + protected function getDefaultInternalConfig() { return array( 'customConfig' => '', diff --git a/core/modules/language/config/language.mappings.yml b/core/modules/language/config/language.mappings.yml index e4ef2a1788d..f7fc321557a 100644 --- a/core/modules/language/config/language.mappings.yml +++ b/core/modules/language/config/language.mappings.yml @@ -1,5 +1,8 @@ # Browsers use different language codes to refer to the same languages, # these defaults handles the most common cases. +no: 'nb' # Norwegian +pt: 'pt-pt' # Portuguese +zh: 'zh-hans' # Default Chinese to simplified script zh-tw: 'zh-hant' # Taiwan Chinese in traditional script zh-hk: 'zh-hant' # Hong Kong Chinese in traditional script zh-mo: 'zh-hant' # Macao Chinese in traditional script diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index 88348128ba7..dc69597f44a 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -111,7 +111,13 @@ function language_from_browser($languages) { // so we multiply the qvalue by 1000 to avoid floating point comparisons. $langcode = strtolower($match[1]); $qvalue = isset($match[2]) ? (float) $match[2] : 1; - $browser_langcodes[$langcode] = (int) ($qvalue * 1000); + // Take the highest qvalue for this langcode. Although the request + // supposedly contains unique langcodes, our mapping possibly resolves + // to the same langcode for different qvalues. Keep the highest. + $browser_langcodes[$langcode] = max( + (int) ($qvalue * 1000), + (isset($browser_langcodes[$langcode]) ? $browser_langcodes[$langcode] : 0) + ); } }