diff --git a/core/lib/Drupal/Core/Language/Language.php b/core/lib/Drupal/Core/Language/Language.php index ec3ce71..7702496 100644 --- a/core/lib/Drupal/Core/Language/Language.php +++ b/core/lib/Drupal/Core/Language/Language.php @@ -80,7 +80,9 @@ class Language implements LanguageInterface { public function __construct(array $values = array()) { // Set all the provided properties for the language. foreach ($values as $key => $value) { - $this->{$key} = $value; + if (property_exists($this, $key)) { + $this->{$key} = $value; + } } // If some values were not set, set sane defaults of a predefined language. if (!isset($values['name']) || !isset($values['direction'])) { diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index e0916e4..aac3c65 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -27,11 +27,17 @@ class LanguageManager implements LanguageManagerInterface { protected $translation; /** - * An array of all the available languages keyed by language code. + * A static cache of translated language lists. + * + * Array of arrays to cache the result of self::getLanguages() keyed by the + * language the list is translated to (first level) and the flags provided to + * the method (second level). * * @var \Drupal\Core\Language\LanguageInterface[] + * + * @see \Drupal\Core\Language\LanguageManager::getLanguages() */ - protected $languages; + protected $languages = array(); /** * The default language object. @@ -130,35 +136,19 @@ public function getDefaultLanguage() { * {@inheritdoc} */ public function getLanguages($flags = LanguageInterface::STATE_CONFIGURABLE) { - // Initialize master language list. - if (!isset($this->languages)) { - // No language module, so use the default language only. + $static_cache_id = $this->getCurrentLanguage()->getId(); + if (!isset($this->languages[$static_cache_id][$flags])) { + // If this language manager is used, there are no configured languages. + // The default language and locked languages comprise the full language + // list. $default = $this->getDefaultLanguage(); - $this->languages = array($default->getId() => $default); - // Add the special languages, they will be filtered later if needed. - $this->languages += $this->getDefaultLockedLanguages($default->getWeight()); - } + $languages = array($default->getId() => $default); + $languages += $this->getDefaultLockedLanguages($default->getWeight()); - // Filter the full list of languages based on the value of the $all flag. By - // default we remove the locked languages, but the caller may request for - // those languages to be added as well. - $filtered_languages = array(); - - // Add the site's default language if flagged as allowed value. - if ($flags & LanguageInterface::STATE_SITE_DEFAULT) { - // Setup a language to have the defaults, but with overridden name. - $default = $this->getDefaultLanguage(); - $default->setName($this->t("Site's default language (@lang_name)", array('@lang_name' => $default->getName()))); - $filtered_languages[LanguageInterface::LANGCODE_SITE_DEFAULT] = $default; + // Filter the full list of languages based on the value of $flags. + $this->languages[$static_cache_id][$flags] = $this->filterLanguages($languages, $flags); } - - foreach ($this->languages as $id => $language) { - if (($language->isLocked() && ($flags & LanguageInterface::STATE_LOCKED)) || (!$language->isLocked() && ($flags & LanguageInterface::STATE_CONFIGURABLE))) { - $filtered_languages[$id] = $language; - } - } - - return $filtered_languages; + return $this->languages[$static_cache_id][$flags]; } /** @@ -382,4 +372,44 @@ public function getConfigOverrideLanguage() { return $this->getCurrentLanguage(); } + + /** + * Filters the full list of languages based on the value of the flag. + * + * The locked languages are removed by default. + * + * @param \Drupal\Core\Language\LanguageInterface[] $languages + * Array with languages to be filtered. + * @param int $flags + * (optional) Specifies the state of the languages that have to be returned. + * It can be: LanguageInterface::STATE_CONFIGURABLE, + * LanguageInterface::STATE_LOCKED, or LanguageInterface::STATE_ALL. + * + * @return \Drupal\Core\Language\LanguageInterface[] + * An associative array of languages, keyed by the language code. + */ + protected function filterLanguages(array $languages, $flags = LanguageInterface::STATE_CONFIGURABLE) { + // STATE_ALL means we don't actually filter, so skip the rest of the method. + if ($flags == LanguageInterface::STATE_ALL) { + return $languages; + } + + $filtered_languages = array(); + // Add the site's default language if requested. + if ($flags & LanguageInterface::STATE_SITE_DEFAULT) { + // Setup a language to have the defaults, but with overridden name. + $default = $this->getDefaultLanguage(); + $default->setName($this->t("Site's default language (@lang_name)", array('@lang_name' => $default->getName()))); + $filtered_languages[LanguageInterface::LANGCODE_SITE_DEFAULT] = $default; + } + + foreach ($languages as $id => $language) { + if (($language->isLocked() && ($flags & LanguageInterface::STATE_LOCKED)) || (!$language->isLocked() && ($flags & LanguageInterface::STATE_CONFIGURABLE))) { + $filtered_languages[$id] = $language; + } + } + + return $filtered_languages; + } + } diff --git a/core/modules/language/src/ConfigurableLanguageManager.php b/core/modules/language/src/ConfigurableLanguageManager.php index 534f232..e17b3d2 100644 --- a/core/modules/language/src/ConfigurableLanguageManager.php +++ b/core/modules/language/src/ConfigurableLanguageManager.php @@ -17,7 +17,6 @@ use Drupal\Core\Url; use Drupal\language\Config\LanguageConfigFactoryOverrideInterface; use Drupal\language\Entity\ConfigurableLanguage; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** @@ -247,7 +246,7 @@ public function reset($type = NULL) { $this->negotiatedMethods = array(); $this->languageTypes = NULL; $this->languageTypesInfo = NULL; - $this->languages = NULL; + $this->languages = array(); if ($this->negotiator) { $this->negotiator->reset(); } @@ -279,37 +278,47 @@ public function setNegotiator(LanguageNegotiatorInterface $negotiator) { * {@inheritdoc} */ public function getLanguages($flags = LanguageInterface::STATE_CONFIGURABLE) { - if (!isset($this->languages)) { - // Prepopulate the language list with the default language to keep things - // working even if we have no configuration. - $default = $this->getDefaultLanguage(); - $this->languages = array($default->getId() => $default); - - // Retrieve the list of languages defined in configuration. - $prefix = 'language.entity.'; - $config_ids = $this->configFactory->listAll($prefix); + // If a config override is set, cache using that language's ID. + if ($override_language = $this->getConfigOverrideLanguage()) { + $static_cache_id = $override_language->getId(); + } + else { + $static_cache_id = $this->getCurrentLanguage()->getId(); + } - // Instantiate languages from config objects. - $weight = 0; + if (!isset($this->languages[$static_cache_id][$flags])) { + // Initialize the language list with the default language and default + // locked languages. These cannot be removed. This serves as a fallback + // list if this method is invoked while the language module is installed + // and the configuration entities for languages are not yet fully + // imported. + $default = $this->getDefaultLanguage(); + $languages = array($default->getId() => $default); + $languages += $this->getDefaultLockedLanguages($default->getWeight()); + + // Load configurable languages on top of the defaults. Ideally this could + // use the entity API to load and instantiate ConfigurableLanguage + // objects. However the entity API depends on the language system, so that + // would result in infinite loops. We use the configuration system + // directly and instantiate runtime Language objects. When language + // entities are imported those cover the default and locked languages, so + // site-specific configuration will prevail over the fallback values. + // Having them in the array already ensures if this is invoked in the + // middle of importing language configuration entities, the defaults are + // always present. + $config_ids = $this->configFactory->listAll('language.entity.'); foreach ($this->configFactory->loadMultiple($config_ids) as $config) { $data = $config->get(); - $langcode = $data['id']; - // Initialize default property so callers have an easy reference and can - // save the same object without data loss. - $data['default'] = ($langcode == $default->getId()); $data['name'] = $data['label']; - $this->languages[$langcode] = new Language($data); - $weight = max(array($weight, $this->languages[$langcode]->getWeight())); + $languages[$data['id']] = new Language($data); } + Language::sort($languages); - // Add locked languages, they will be filtered later if needed. - $this->languages += $this->getDefaultLockedLanguages($weight); - - // Sort the language list by weight then title. - Language::sort($this->languages); + // Filter the full list of languages based on the value of $flags. + $this->languages[$static_cache_id][$flags] = $this->filterLanguages($languages, $flags); } - return parent::getLanguages($flags); + return $this->languages[$static_cache_id][$flags]; } /** diff --git a/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php b/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php new file mode 100644 index 0000000..15290fd --- /dev/null +++ b/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php @@ -0,0 +1,93 @@ +administrator = $this->drupalCreateUser($this->getAdministratorPermissions(), 'administrator'); + $this->drupalLogin($this->administrator); + } + + /** + * Returns an array of permissions needed for the translator. + */ + protected function getAdministratorPermissions() { + return array_filter( + array('translate interface', + 'administer content translation', + 'create content translations', + 'update content translations', + 'delete content translations', + 'administer languages', + ) + ); + } + + /** + * Tests content translation language selectors are correctly translated. + */ + public function testLanguageStringSelector() { + // Add another language. + $edit = array('predefined_langcode' => 'es'); + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + // Translate the string English in Spanish (Inglés). Override config entity. + $name_translation = 'Inglés'; + \Drupal::languageManager() + ->getLanguageConfigOverride('es', 'language.entity.en') + ->set('label', $name_translation) + ->save(); + + // Check content translation overview selector. + $path = 'es/admin/config/regional/content-language'; + $this->drupalGet($path); + + // Get en language from selector. + $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => 'edit-settings-node-node-settings-language-langcode', ':option' => 'en')); + + // Check that the language text is translated. + $this->assertEqual((string) $elements[0], $name_translation, 'Checking the option string English is translated to Spanish.'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Language/LanguageUnitTest.php b/core/tests/Drupal/Tests/Core/Language/LanguageUnitTest.php index e21ab6f..49a9dbf 100644 --- a/core/tests/Drupal/Tests/Core/Language/LanguageUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Language/LanguageUnitTest.php @@ -18,6 +18,20 @@ class LanguageUnitTest extends UnitTestCase { /** + * @covers ::__construct + */ + public function testConstruct() { + $name = $this->randomMachineName(); + $language_code = $this->randomMachineName(2); + $uuid = $this->randomMachineName(); + $language = new Language(array('id' => $language_code, 'name' => $name, 'uuid' => $uuid)); + // Test that nonexistent properties are not added to the language object. + $this->assertTrue(property_exists($language, 'id')); + $this->assertTrue(property_exists($language, 'name')); + $this->assertFalse(property_exists($language, 'uuid')); + } + + /** * @covers ::getName() * @covers ::setName() */