diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php index 18d2ee8..d7bc941 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php @@ -67,12 +67,12 @@ static function getSubscribedEvents() { */ protected function filterOverride(Config $config, StorableConfigBase $override) { $override_data = $override->get(); - $this->filterNestedArray($config->get(), $override_data); + $changed = $this->filterNestedArray($config->get(), $override_data); if (empty($override_data)) { // If no override values are left that would apply, remove the override. $override->delete(); } - else { + elseif ($changed) { // Otherwise set the filtered override values back. $override->setData($override_data)->save(); } @@ -85,29 +85,37 @@ protected function filterOverride(Config $config, StorableConfigBase $override) * Original data array to filter against. * @param array $override_data * Override data to filter. + * + * @return bool + * TRUE if $override_data was changed, FALSE otherwise. */ protected function filterNestedArray(array $original_data, array &$override_data) { + $changed = FALSE; foreach ($override_data as $key => $value) { if (!isset($original_data[$key])) { // The original data is not there anymore, remove the override. unset($override_data[$key]); + $changed = TRUE; } elseif (is_array($override_data[$key])) { if (is_array($original_data[$key])) { // Do the filtering one level deeper. - $this->filterNestedArray($original_data[$key], $override_data[$key]); + $changed = $this->filterNestedArray($original_data[$key], $override_data[$key]); // If no overrides are left under this level, remove the level. if (empty($override_data[$key])) { unset($override_data[$key]); + $changed = TRUE; } } else { // The override is an array but the value is not, this will not go // well, remove the override. unset($override_data[$key]); + $changed = TRUE; } } } + return $changed; } } diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index 5127adc..00bbba5 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -53,7 +53,7 @@ public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $co if (!$typed_config->hasConfigSchema($config_name)) { return FALSE; } - $definition = $typed_config->getDefinition($config_name); + $definition = $typed_config->getDefinition($config_name, TRUE, TRUE); $data_definition = $typed_config->buildDataDefinition($definition, $config_data); $this->schema = $typed_config->create($data_definition, $config_data); $errors = array(); diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php index 4af470e..d4b1fb1 100644 --- a/core/lib/Drupal/Core/Config/StorableConfigBase.php +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php @@ -128,7 +128,7 @@ public function getStorage() { */ protected function getSchemaWrapper() { if (!isset($this->schemaWrapper)) { - $definition = $this->typedConfigManager->getDefinition($this->name); + $definition = $this->typedConfigManager->getDefinition($this->name, TRUE, TRUE); $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data); $this->schemaWrapper = $this->typedConfigManager->create($data_definition, $this->data); } diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index 5f71e6b..a086e34 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -72,8 +72,8 @@ public function __construct(StorageInterface $configStorage, StorageInterface $s */ public function get($name) { $data = $this->configStorage->read($name); - $type_definition = $this->getDefinition($name); - $data_definition = $this->buildDataDefinition($type_definition, $data); + $type_definition = $this->getDefinition($name, TRUE, TRUE); + $data_definition = $this->buildDataDefinition($type_definition, $data); return $this->create($data_definition, $data); } @@ -116,7 +116,7 @@ public function buildDataDefinition(array $definition, $value, $name = NULL, $pa /** * {@inheritdoc} */ - public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) { + public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE, $is_config_name = FALSE) { $definitions = $this->getDefinitions(); if (isset($definitions[$base_plugin_id])) { $type = $base_plugin_id; @@ -141,10 +141,19 @@ public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) { $this->definitions[$type] = $definition; } // Add type and default definition class. - return $definition + array( + $definition += array( 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', 'type' => $type, ); + // If this is definition expected for a config name and it is defined as a + // mapping, add a langcode element if not already present. + if ($is_config_name && isset($definition['mapping']) && !isset($definition['mapping']['langcode'])) { + $definition['mapping']['langcode'] = array( + 'type' => 'string', + 'label' => 'Language code', + ); + } + return $definition; } /** diff --git a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php index 84d347b..402cb82 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php @@ -102,4 +102,24 @@ public function buildDataDefinition(array $definition, $value, $name = NULL, $pa */ public function hasConfigSchema($name); + /** + * Gets a specific plugin definition. + * + * @param string $plugin_id + * A plugin id. + * @param bool $exception_on_invalid + * Ignored with TypedConfigManagerInterface. Kept for compatibility with + * DiscoveryInterface. + * @param bool $is_config_name + * Set to TRUE if $plugin_id is a configuration name (as opposed to an + * internal configuration schema type). + * + * @return array + * A plugin definition array. If the given plugin id does not have typed + * configuration definition assigned, the definition of an undefined + * element type is returned. If $is_config_name is set, a langcode key + * is automatically added to the definition. + */ + public function getDefinition($plugin_id, $exception_on_invalid = TRUE, $is_config_name = FALSE); + } diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php index 1285cb7..e2c1a98 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -60,6 +60,29 @@ public function __construct($string, array $arguments = array(), array $options } /** + * Gets the untranslated string value stored in this translation wrapper. + * + * @return string + * The string stored in this wrapper. + */ + public function getUntranslatedString() { + return $this->string; + } + + /** + * Gets a specific option from this translation wrapper. + * + * @param $name + * Option name. + * + * @return mixed + * The value of this option or empty string of option is not set. + */ + public function getOption($name) { + return isset($this->options[$name]) ? $this->options[$name] : ''; + } + + /** * Implements the magic __toString() method. */ public function __toString() { diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php index 676401e..cf945d9 100644 --- a/core/modules/config/src/Tests/ConfigSchemaTest.php +++ b/core/modules/config/src/Tests/ConfigSchemaTest.php @@ -26,7 +26,7 @@ class ConfigSchemaTest extends KernelTestBase { * * @var array */ - public static $modules = array('system', 'language', 'locale', 'field', 'image', 'config_test', 'config_schema_test'); + public static $modules = array('system', 'language', 'field', 'image', 'config_test', 'config_schema_test'); /** * {@inheritdoc} diff --git a/core/modules/config_translation/src/ConfigEntityMapper.php b/core/modules/config_translation/src/ConfigEntityMapper.php index 08ce8a6..fe3da3c 100644 --- a/core/modules/config_translation/src/ConfigEntityMapper.php +++ b/core/modules/config_translation/src/ConfigEntityMapper.php @@ -98,7 +98,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('config.factory'), $container->get('config.typed'), - $container->get('locale.config.typed'), + $container->get('locale.config_manager'), $container->get('plugin.manager.config_translation.mapper'), $container->get('router.route_provider'), $container->get('string_translation'), diff --git a/core/modules/config_translation/src/ConfigMapperInterface.php b/core/modules/config_translation/src/ConfigMapperInterface.php index ac3d531..f794a10 100644 --- a/core/modules/config_translation/src/ConfigMapperInterface.php +++ b/core/modules/config_translation/src/ConfigMapperInterface.php @@ -205,22 +205,6 @@ public function getConfigData(); public function getLangcode(); /** - * Returns language object for the configuration. - * - * If the language of the configuration files is not a configured language on - * the site and it is English, we return a dummy language object to represent - * the built-in language. - * - * @return \Drupal\Core\Language\LanguageInterface - * A configured language object instance or a dummy English language object. - * - * @throws \RuntimeException - * Throws an exception if the language codes in the config files don't - * match. - */ - public function getLanguageWithFallback(); - - /** * Returns the name of the type of data the mapper encapsulates. * * @return string diff --git a/core/modules/config_translation/src/ConfigNamesMapper.php b/core/modules/config_translation/src/ConfigNamesMapper.php index b681b33..ca4e8d8 100644 --- a/core/modules/config_translation/src/ConfigNamesMapper.php +++ b/core/modules/config_translation/src/ConfigNamesMapper.php @@ -150,7 +150,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('config.factory'), $container->get('config.typed'), - $container->get('locale.config.typed'), + $container->get('locale.config_manager'), $container->get('plugin.manager.config_translation.mapper'), $container->get('router.route_provider'), $container->get('string_translation'), @@ -410,24 +410,6 @@ public function getLangcode() { /** * {@inheritdoc} */ - public function getLanguageWithFallback() { - $langcode = $this->getLangcode(); - $language = $this->languageManager->getLanguage($langcode); - // If the language of the file is English but English is not a configured - // language on the site, create a mock language object to represent this - // language run-time. In this case, the title of the language is - // 'Built-in English' because we assume such configuration is shipped with - // core and the modules and not custom created. (In the later case an - // English language configured on the site is assumed.) - if (empty($language) && $langcode == 'en') { - $language = new Language(array('id' => 'en', 'name' => $this->t('Built-in English'))); - } - return $language; - } - - /** - * {@inheritdoc} - */ public function getConfigData() { $config_data = array(); foreach ($this->getConfigNames() as $name) { @@ -465,7 +447,7 @@ public function hasTranslatable() { */ public function hasTranslation(LanguageInterface $language) { foreach ($this->getConfigNames() as $name) { - if ($this->localeConfigManager->hasTranslation($name, $language)) { + if ($this->localeConfigManager->hasTranslation($name, $language->getId())) { return TRUE; } } diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationController.php b/core/modules/config_translation/src/Controller/ConfigTranslationController.php index 72fe9cc..9c7fd67 100644 --- a/core/modules/config_translation/src/Controller/ConfigTranslationController.php +++ b/core/modules/config_translation/src/Controller/ConfigTranslationController.php @@ -127,18 +127,12 @@ public function itemPage(Request $request, RouteMatchInterface $route_match, $pl $page = array(); $page['#title'] = $this->t('Translations for %label', array('%label' => $mapper->getTitle())); - // It is possible the original language this configuration was saved with is - // not on the system. For example, the configuration shipped in English but - // the site has no English configured. Represent the original language in - // the table even if it is not currently configured. $languages = $this->languageManager->getLanguages(); $original_langcode = $mapper->getLangcode(); if (!isset($languages[$original_langcode])) { + // If the language is not configured on the site, create a dummy language + // object for this listing only to ensure the user gets useful info. $language_name = $this->languageManager->getLanguageName($original_langcode); - if ($original_langcode == 'en') { - $language_name = $this->t('Built-in English'); - } - // Create a dummy language object for this listing only. $languages[$original_langcode] = new Language(array('id' => $original_langcode, 'name' => $language_name)); } diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php index 2831fd9..9e5372f 100644 --- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php +++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php @@ -144,7 +144,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $ $this->mapper = $mapper; $this->language = $language; - $this->sourceLanguage = $this->mapper->getLanguageWithFallback(); + $this->sourceLanguage = $this->languageManager->getLanguage($this->mapper->getLangcode()); // Get base language configuration to display in the form before setting the // language to use for the form. This avoids repetitively settings and diff --git a/core/modules/config_translation/tests/src/Unit/ConfigNamesMapperTest.php b/core/modules/config_translation/tests/src/Unit/ConfigNamesMapperTest.php index 0b3ead9..796012f 100644 --- a/core/modules/config_translation/tests/src/Unit/ConfigNamesMapperTest.php +++ b/core/modules/config_translation/tests/src/Unit/ConfigNamesMapperTest.php @@ -589,7 +589,7 @@ public function testHasTranslation(array $mock_return_values, $expected) { $map = array(); foreach ($config_names as $i => $config_name) { - $map[] = array($config_name, $language, $mock_return_values[$i]); + $map[] = array($config_name, $language->getId(), $mock_return_values[$i]); } $this->localeConfigManager ->expects($this->any()) diff --git a/core/modules/language/language.module b/core/modules/language/language.module index f66bff2..ae8c2e2 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -161,21 +161,6 @@ function language_process_language_select($element) { $element['#options'][$langcode] = $language->isLocked() ? t('- @name -', array('@name' => $language->getName())) : $language->getName(); } } - // Add "Built-in English" language to the select when the default value is - // set to English but it does not exist in the options list. - // - // Drupal core includes configuration shipped in English, including default - // views, content types, user roles, filter formats, etc. To keep the Drupal - // software update-able, as well as translations update-able, we keep these - // configuration files in English even when installed in a foreign language. - // However, administrators can remove English, in which case editing such a - // configuration would lead to the language settings being changed on it. We - // avoid that by including this option and letting administrators keep it - // in English. - if (isset($element['#default_value']) && $element['#default_value'] == 'en' && !isset($element['#options']['en'])) { - // Prepend the default language at the beginning of the list. - $element['#options'] = array('en' => t('Built-in English')) + $element['#options']; - } return $element; } diff --git a/core/modules/language/src/Entity/ConfigurableLanguage.php b/core/modules/language/src/Entity/ConfigurableLanguage.php index 8fef8d2..89cc167 100644 --- a/core/modules/language/src/Entity/ConfigurableLanguage.php +++ b/core/modules/language/src/Entity/ConfigurableLanguage.php @@ -117,10 +117,6 @@ public function preSave(EntityStorageInterface $storage) { // rebuild services if necessary during // \Drupal\language\Entity\ConfigurableLanguage::postSave(). $this->preSaveMultilingual = \Drupal::languageManager()->isMultilingual(); - // Languages are picked from a predefined list which is given in English. - // For the uncommon case of custom languages the label should be given in - // English. - $this->langcode = 'en'; } /** diff --git a/core/modules/language/src/Form/LanguageFormBase.php b/core/modules/language/src/Form/LanguageFormBase.php index a272166..14f219a 100644 --- a/core/modules/language/src/Form/LanguageFormBase.php +++ b/core/modules/language/src/Form/LanguageFormBase.php @@ -75,7 +75,7 @@ public function commonForm(array &$form) { } $form['label'] = array( '#type' => 'textfield', - '#title' => $this->t('Language name in English'), + '#title' => $this->t('Language name'), '#maxlength' => 64, '#default_value' => $language->label(), '#required' => TRUE, diff --git a/core/modules/language/src/Tests/LanguageConfigurationTest.php b/core/modules/language/src/Tests/LanguageConfigurationTest.php index dc522bc..fdaf217 100644 --- a/core/modules/language/src/Tests/LanguageConfigurationTest.php +++ b/core/modules/language/src/Tests/LanguageConfigurationTest.php @@ -132,7 +132,7 @@ function testLanguageConfiguration() { ); $this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language'); $language = $this->config('language.entity.de')->get(); - $this->assertEqual($language['langcode'], 'en'); + $this->assertEqual($language['langcode'], 'fr'); // Ensure that German language has a weight of 5 after being created through // the UI. diff --git a/core/modules/language/src/Tests/LanguageCustomLanguageConfigurationTest.php b/core/modules/language/src/Tests/LanguageCustomLanguageConfigurationTest.php index fb74878..da6ddf5 100644 --- a/core/modules/language/src/Tests/LanguageCustomLanguageConfigurationTest.php +++ b/core/modules/language/src/Tests/LanguageCustomLanguageConfigurationTest.php @@ -41,7 +41,7 @@ public function testLanguageConfiguration() { $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Test validation on missing values. $this->assertText(t('!name field is required.', array('!name' => t('Language code')))); - $this->assertText(t('!name field is required.', array('!name' => t('Language name in English')))); + $this->assertText(t('!name field is required.', array('!name' => t('Language name')))); $empty_language = new Language(); $this->assertFieldChecked('edit-direction-' . $empty_language->getDirection(), 'Consistent usage of language direction.'); $this->assertUrl(\Drupal::url('language.add', array(), array('absolute' => TRUE)), [], 'Correct page redirection.'); @@ -60,7 +60,7 @@ public function testLanguageConfiguration() { '@url' => 'http://www.w3.org/International/articles/language-tags/', ))); - $this->assertRaw(t('%field cannot contain any markup.', array('%field' => t('Language name in English')))); + $this->assertRaw(t('%field cannot contain any markup.', array('%field' => t('Language name')))); $this->assertUrl(\Drupal::url('language.add', array(), array('absolute' => TRUE)), [], 'Correct page redirection.'); // Test adding a custom language with a numeric region code. diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 357aaf8..a4677ca 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -324,7 +324,7 @@ function locale_translate_batch_refresh(array &$context) { } elseif ($name = array_shift($context['sandbox']['refresh']['names'])) { // Refresh all languages for one object at a time. - $count = locale_config_update_multiple(array($name), $context['sandbox']['refresh']['languages']); + $count = Locale::config()->updateConfigTranslations(array($name), $context['sandbox']['refresh']['languages']); $context['results']['stats']['config'] += $count; // Inherit finished information from the "parent" string lookup step so // visual display of status will make sense. @@ -532,7 +532,7 @@ function locale_translate_delete_translation_files(array $projects = array(), ar * The batch definition. */ function locale_config_batch_update_components(array $options, array $langcodes = array(), array $components = array()) { - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(\Drupal::languageManager()->getLanguages()); if ($langcodes && $names = \Drupal\locale\Locale::config()->getComponentNames($components)) { return locale_config_batch_build($names, $langcodes, $options); } @@ -606,7 +606,7 @@ function locale_config_batch_refresh_name(array $names, array $langcodes, array if (!isset($context['result']['stats']['config'])) { $context['result']['stats']['config'] = 0; } - $context['result']['stats']['config'] += locale_config_update_multiple($names, $langcodes); + $context['result']['stats']['config'] += Locale::config()->updateConfigTranslations($names, $langcodes); foreach ($names as $name) { $context['result']['names'][] = $name; } @@ -639,41 +639,3 @@ function locale_config_batch_finished($success, array $results) { } } } - -/** - * Updates all configuration for names / languages. - * - * @param array $names - * Array of names of configuration objects to update. - * @param array $langcodes - * (optional) Array of language codes to update. Defaults to all languages. - * - * @return int - * Number of configuration objects retranslated. - */ -function locale_config_update_multiple(array $names, array $langcodes = array()) { - /** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */ - $language_manager = \Drupal::languageManager(); - $locale_config_manager = Locale::config(); - - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - $count = 0; - foreach ($names as $name) { - $wrapper = $locale_config_manager->get($name); - foreach ($langcodes as $langcode) { - $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode)->getValue() : NULL; - if ($translation) { - $locale_config_manager->saveTranslationData($name, $langcode, $translation); - $count++; - } - else { - // Do not bother deleting language overrides which do not exist in the - // first place. - if (!$language_manager->getLanguageConfigOverride($langcode, $name)->isNew()) { - $locale_config_manager->deleteTranslationData($name, $langcode); - } - } - } - } - return $count; -} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 971b47d..42a6772 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -24,6 +24,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\language\ConfigurableLanguageInterface; use Drupal\Component\Utility\Crypt; +use Drupal\locale\Locale; /** * Regular expression pattern used to localize JavaScript strings. @@ -226,7 +227,7 @@ function locale_configurable_language_delete(ConfigurableLanguageInterface $lang locale_translate_delete_translation_files(array(), array($language->id())); // Remove translated configuration objects. - \Drupal\locale\Locale::config()->deleteLanguageTranslations($language->id()); + Locale::config()->deleteLanguageTranslations($language->id()); // Changing the language settings impacts the interface: _locale_invalidate_js($language->id()); @@ -246,7 +247,7 @@ function locale_configurable_language_delete(ConfigurableLanguageInterface $lang */ function locale_translatable_language_list() { $languages = \Drupal::languageManager()->getLanguages(); - if (!locale_translate_english()) { + if (!locale_is_translatable('en')) { unset($languages['en']); } return $languages; @@ -312,6 +313,8 @@ function locale_get_plural($count, $langcode = NULL) { * Implements hook_modules_installed(). */ function locale_modules_installed($modules) { + locale_system_set_config_langcodes(); + $components['module'] = $modules; locale_system_update($components); } @@ -328,6 +331,8 @@ function locale_module_preuninstall($module) { * Implements hook_themes_installed(). */ function locale_themes_installed($themes) { + locale_system_set_config_langcodes(); + $components['theme'] = $themes; locale_system_update($components); } @@ -356,6 +361,34 @@ function locale_cron() { } /** + * Updates default configuration when new modules or themes are installed. + */ +function locale_system_set_config_langcodes() { + // Need to rewrite some default configuration language codes if the default + // site language is not English. + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + if ($default_langcode != 'en') { + // Update active configuration copies of all prior shipped configuration if + // they are still English. It is not enough to change configuration shipped + // with the components just installed, because installing a component such + // as views or tour module may bring in default configuration from prior + // components. + $names = Locale::config()->getComponentNames(); + foreach ($names as $name) { + $config = \Drupal::configFactory()->reset($name)->getEditable($name); + // Should only update if still exists in active configuration. If locale + // module is enabled later, then some configuration may not exist anymore. + if (!$config->isNew()) { + $langcode = $config->get('langcode'); + if (empty($langcode) || $langcode == 'en') { + $config->set('langcode', $default_langcode)->save(); + } + } + } + } +} + +/** * Imports translations when new modules or themes are installed. * * This function will start a batch to import translations for the added @@ -411,8 +444,6 @@ function locale_system_remove($components) { if ($language_list = locale_translatable_language_list()) { module_load_include('compare.inc', 'locale'); \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); - // Delete configuration translations. - \Drupal\locale\Locale::config()->deleteComponentTranslations($components, array_keys($language_list)); // Only when projects are removed, the translation files and records will be // deleted. Not each disabled module will remove a project, e.g., sub @@ -606,7 +637,7 @@ function locale_form_language_admin_overview_form_alter(&$form, FormStateInterfa 'translated' => 0, 'ratio' => 0, ); - if (!$language->isLocked() && ($langcode != 'en' || locale_translate_english())) { + if (!$language->isLocked() && locale_is_translatable($langcode)) { $form['languages'][$langcode]['locale_statistics'] = array( '#markup' => \Drupal::l( t('@translated/@total (@ratio%)', array( @@ -645,21 +676,26 @@ function locale_form_language_admin_add_form_alter(&$form, FormStateInterface $f * Set a batch for a newly-added language. */ function locale_form_language_admin_add_form_alter_submit($form, FormStateInterface $form_state) { - if (\Drupal::config('locale.settings')->get('translation.import_enabled')) { - if ($form_state->isValueEmpty('predefined_langcode') || $form_state->getValue('predefined_langcode') == 'custom') { - $langcode = $form_state->getValue('langcode'); - } - else { - $langcode = $form_state->getValue('predefined_langcode'); - } + \Drupal::moduleHandler()->loadInclude('locale', 'fetch.inc'); + $options = _locale_translation_default_update_options(); + if ($form_state->isValueEmpty('predefined_langcode') || $form_state->getValue('predefined_langcode') == 'custom') { + $langcode = $form_state->getValue('langcode'); + } + else { + $langcode = $form_state->getValue('predefined_langcode'); + } + + if (\Drupal::config('locale.settings')->get('translation.import_enabled')) { // Download and import translations for the newly added language. - module_load_include('fetch.inc', 'locale'); - $options = _locale_translation_default_update_options(); $batch = locale_translation_batch_update_build(array(), array($langcode), $options); batch_set($batch); + } - // Create or update all configuration translations for this language. + // Create or update all configuration translations for this language. If we + // are adding English then we need to run this even if import is not enabled, + // because then we extract English sources from shipped configuration. + if (\Drupal::config('locale.settings')->get('translation.import_enabled') || $langcode == 'en') { \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); if ($batch = locale_config_batch_update_components($options, array($langcode))) { batch_set($batch); @@ -689,13 +725,16 @@ function locale_form_language_admin_edit_form_alter_submit($form, FormStateInter } /** - * Checks whether locale translates to English. + * Checks whether $langcode is a language supported as a locale target. + * + * @param string $langcode + * The language code. * * @return bool - * Returns TRUE if content should be translated to English, FALSE otherwise. + * Whether $langcode can be translated to in locale. */ -function locale_translate_english() { - return \Drupal::config('locale.settings')->get('translate_english'); +function locale_is_translatable($langcode) { + return $langcode != 'en' || \Drupal::config('locale.settings')->get('translate_english'); } /** @@ -1020,19 +1059,14 @@ function _locale_refresh_translations($langcodes, $lids = array()) { /** * Refreshes configuration after string translations have been updated. * - * The information that will be refreshed includes: - * - JavaScript translations. - * - Locale cache. - * * @param array $langcodes * Language codes for updated translations. * @param array $lids * List of string identifiers that have been updated / created. */ function _locale_refresh_configuration(array $langcodes, array $lids) { - if ($lids && $langcodes && $names = \Drupal\locale\Locale::config()->getStringNames($lids)) { - \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); - locale_config_update_multiple($names, $langcodes); + if ($lids && $langcodes && $names = Locale::config()->getStringNames($lids)) { + Locale::config()->updateConfigTranslations($names, $langcodes); } } diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index 3df6f93..284878b 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -1,5 +1,5 @@ services: - locale.config.typed: + locale.config_manager: class: Drupal\locale\LocaleConfigManager arguments: ['@config.storage', '@config.storage.installer', '@locale.storage', '@config.factory', '@config.typed', '@language_manager'] locale.storage: @@ -22,6 +22,6 @@ services: - { name: stream_wrapper, scheme: translations } locale.config_subscriber: class: Drupal\locale\LocaleConfigSubscriber - arguments: ['@locale.storage', '@config.factory', '@locale.config.typed'] + arguments: ['@config.factory', '@locale.config_manager'] tags: - { name: event_subscriber } diff --git a/core/modules/locale/src/Form/ExportForm.php b/core/modules/locale/src/Form/ExportForm.php index f9b6361..035b2e8 100644 --- a/core/modules/locale/src/Form/ExportForm.php +++ b/core/modules/locale/src/Form/ExportForm.php @@ -61,7 +61,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $languages = $this->languageManager->getLanguages(); $language_options = array(); foreach ($languages as $langcode => $language) { - if ($langcode != 'en' || locale_translate_english()) { + if (locale_is_translatable($langcode)) { $language_options[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Form/ImportForm.php b/core/modules/locale/src/Form/ImportForm.php index e3448b3..ca706ea 100644 --- a/core/modules/locale/src/Form/ImportForm.php +++ b/core/modules/locale/src/Form/ImportForm.php @@ -79,7 +79,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // are to translate Drupal to English as well. $existing_languages = array(); foreach ($languages as $langcode => $language) { - if ($langcode != 'en' || locale_translate_english()) { + if (locale_is_translatable($langcode)) { $existing_languages[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Form/TranslateFormBase.php b/core/modules/locale/src/Form/TranslateFormBase.php index b894f17..59af72b 100644 --- a/core/modules/locale/src/Form/TranslateFormBase.php +++ b/core/modules/locale/src/Form/TranslateFormBase.php @@ -164,7 +164,7 @@ protected function translateFilters() { $languages = $this->languageManager->getLanguages(); $language_options = array(); foreach ($languages as $langcode => $language) { - if ($langcode != 'en' || locale_translate_english()) { + if (locale_is_translatable($langcode)) { $language_options[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Locale.php b/core/modules/locale/src/Locale.php index f4afaed..9235a9c 100644 --- a/core/modules/locale/src/Locale.php +++ b/core/modules/locale/src/Locale.php @@ -23,6 +23,6 @@ class Locale { * @return \Drupal\locale\LocaleConfigManager */ public static function config() { - return \Drupal::service('locale.config.typed'); + return \Drupal::service('locale.config_manager'); } } diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index aee2d59..17d1e8f 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -7,33 +7,54 @@ namespace Drupal\locale; -use Drupal\Core\Config\TypedConfigManagerInterface; -use Drupal\Core\Config\StorageInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\Core\StringTranslation\TranslationWrapper; +use Drupal\Core\TypedData\TraversableTypedDataInterface; +use Drupal\Core\TypedData\TypedDataInterface; use Drupal\language\ConfigurableLanguageManagerInterface; /** - * Manages localized configuration type plugins. + * Manages configuration supported in part by interface translation. + * + * This manager is responsible to update configuration overrides and active + * translations when interface translation data changes. This allows Drupal to + * translate user roles, views, blocks, etc. after Drupal has been installed + * using the locale module's storage. When translations change in locale, + * LocaleConfigManager::updateConfigTranslations() is invoked to update the + * corresponding storage of the translation in the original config object or an + * override. + * + * In turn when translated configuration or configuration language overrides are + * changed, it is the responsibility of LocaleConfigSubscriber to update locale + * storage. + * + * By design locale module only deals with sources in English. + * + * @see \Drupal\locale\LocaleConfigSubscriber */ class LocaleConfigManager { /** - * A storage instance for reading configuration data. + * The storage instance for reading configuration data. * * @var \Drupal\Core\Config\StorageInterface */ protected $configStorage; /** - * A storage instance for reading default configuration data. + * The storage instance for reading default configuration data. * * @var \Drupal\Core\Config\StorageInterface */ protected $installStorage; /** - * A string storage for reading and writing translations. + * The string storage for reading and writing translations. + * + * @var \Drupal\locale\StringStorageInterface; */ protected $localeStorage; @@ -66,11 +87,13 @@ class LocaleConfigManager { protected $typedConfigManager; /** - * Whether or not configuration translations are currently being updated. + * Whether or not configuration translations are being updated from locale. + * + * @see self::isUpdatingFromLocale() * * @var bool */ - protected $isUpdating = FALSE; + protected $isUpdatingFromLocale = FALSE; /** * Creates a new typed configuration manager. @@ -99,61 +122,118 @@ public function __construct(StorageInterface $config_storage, StorageInterface $ } /** - * Gets locale wrapper with typed configuration data. + * Gets array of translation wrappers for translatable configuration. * * @param string $name * Configuration object name. * - * @return \Drupal\locale\LocaleTypedConfig - * Locale-wrapped configuration element. - */ - public function get($name) { - // Read default and current configuration data. - $default = $this->installStorageRead($name); - $updated = $this->configStorage->read($name); - // We get only the data that didn't change from default. - $data = $this->compareConfigData($default, $updated); - $definition = $this->typedConfigManager->getDefinition($name); - $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $data); - // Unless the configuration has a explicit language code we assume English. - $langcode = isset($default['langcode']) ? $default['langcode'] : 'en'; - $wrapper = new LocaleTypedConfig($data_definition, $name, $langcode, $this, $this->typedConfigManager, $this->languageManager); - $wrapper->setValue($data); - return $wrapper; + * @return array + * Array of translatable elements of the default configuration in $name. + */ + public function getTranslatableDefaultConfig($name) { + if ($this->isSupported($name)) { + // Create typed configuration wrapper based on install storage data. + $data = $this->installStorageRead($name); + $type_definition = $this->typedConfigManager->getDefinition($name, TRUE, TRUE); + $data_definition = $this->typedConfigManager->buildDataDefinition($type_definition, $data); + $typed_config = $this->typedConfigManager->create($data_definition, $data); + if ($typed_config instanceof TraversableTypedDataInterface) { + return $this->getTranslatableData($typed_config); + } + } + return array(); } /** - * Compares default configuration with updated data. + * Gets translatable configuration data for a typed configuration element. * - * @param array $default - * Default configuration data. - * @param array|false $updated - * Current configuration data, or FALSE if no configuration data existed. + * @param \Drupal\Core\TypedData\TypedDataInterface $element + * Typed configuration element. * - * @return array - * The elements of default configuration that haven't changed. + * @return array|\Drupal\Core\StringTranslation\TranslationWrapper + * A nested array matching the exact structure under $element with only the + * elements that are translatable wrapped into a TranslationWrapper. If the + * provided $element is not traversable, the return value is a single + * TranslationWrapper. */ - protected function compareConfigData(array $default, $updated) { - // Speed up comparison, specially for install operations. - if ($default === $updated) { - return $default; + protected function getTranslatableData(TypedDataInterface $element) { + $translatable = array(); + if ($element instanceof TraversableTypedDataInterface) { + foreach ($element as $key => $property) { + $value = $this->getTranslatableData($property); + if (!empty($value)) { + $translatable[$key] = $value; + } + } } - $result = array(); - foreach ($default as $key => $value) { - if (isset($updated[$key])) { - if (is_array($value)) { - $result[$key] = $this->compareConfigData($value, $updated[$key]); + else { + $definition = $element->getDataDefinition(); + if (!empty($definition['translatable'])) { + $options = array(); + if (isset($definition['translation context'])) { + $options['context'] = $definition['translation context']; + } + return new TranslationWrapper($element->getValue(), array(), $options); + } + } + return $translatable; + } + + /** + * Process the translatable data array with a given language. + * + * If the given language is translatable, will return the translated copy + * which will only contain strings that had translations. If the given + * language is English and is not translatable, will return a simplified + * array of the English source strings only. + * + * @param string $name + * The configuration name. + * @param array $active + * The active configuration data. + * @param array|\Drupal\Core\StringTranslation\TranslationWrapper[] $translatable + * The translatable array structure. A nested array matching the exact + * structure under of the default configuration for $name with only the + * elements that are translatable wrapped into a TranslationWrapper. + * @see self::getTranslatableData(). + * @param string $langcode + * The language code to process the array with. + * + * @return array + * Processed translatable data array. Will only contain translations + * different from source strings or in case of untranslatable English, the + * source strings themselves. + */ + protected function processTranslatableData($name, array $active, array $translatable, $langcode) { + $translated = array(); + foreach ($translatable as $key => $item) { + if (!isset($active[$key])) { + continue; + } + if (is_array($item)) { + // Only add this key if there was a translated value underneath. + $value = $this->processTranslatableData($name, $active[$key], $item, $langcode); + if (!empty($value)) { + $translated[$key] = $value; + } + } + else { + if (locale_is_translatable($langcode)) { + $value = $this->translateString($name, $langcode, $item->getUntranslatedString(), $item->getOption('context')); + } + else { + $value = $item->getUntranslatedString(); } - elseif ($value === $updated[$key]) { - $result[$key] = $value; + if (!empty($value)) { + $translated[$key] = $value; } } } - return $result; + return $translated; } /** - * Saves translated configuration data. + * Saves translated configuration override. * * @param string $name * Configuration object name. @@ -162,10 +242,24 @@ protected function compareConfigData(array $default, $updated) { * @param array $data * Configuration data to be saved, that will be only the translated values. */ - public function saveTranslationData($name, $langcode, array $data) { - $this->isUpdating = TRUE; + protected function saveTranslationOverride($name, $langcode, array $data) { + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->setData($data)->save(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; + } + + /** + * Saves translated configuration data. + * + * @param string $name + * Configuration object name. + * @param array $data + * Configuration data to be saved with translations merged in. + */ + protected function saveTranslationActive($name, array $data) { + $this->isUpdatingFromLocale = TRUE; + $this->configFactory->getEditable($name)->setData($data)->save(); + $this->isUpdatingFromLocale = FALSE; } /** @@ -176,10 +270,10 @@ public function saveTranslationData($name, $langcode, array $data) { * @param string $langcode * Language code. */ - public function deleteTranslationData($name, $langcode) { - $this->isUpdating = TRUE; + protected function deleteTranslationOverride($name, $langcode) { + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -192,7 +286,7 @@ public function deleteTranslationData($name, $langcode) { * @return array * Array of configuration object names. */ - public function getComponentNames(array $components) { + public function getComponentNames(array $components = array()) { $components = array_filter($components); if ($components) { $names = array(); @@ -209,27 +303,6 @@ public function getComponentNames(array $components) { } /** - * Deletes configuration translations for uninstalled components. - * - * @param array $components - * Array with string identifiers. - * @param array $langcodes - * Array of language codes. - */ - public function deleteComponentTranslations(array $components, array $langcodes) { - $this->isUpdating = TRUE; - $names = $this->getComponentNames($components); - if ($names && $langcodes) { - foreach ($names as $name) { - foreach ($langcodes as $langcode) { - $this->deleteTranslationData($name, $langcode); - } - } - } - $this->isUpdating = FALSE; - } - - /** * Gets configuration names associated with strings. * * @param array $lids @@ -254,12 +327,12 @@ public function getStringNames(array $lids) { * Language code to delete. */ public function deleteLanguageTranslations($langcode) { - $this->isUpdating = TRUE; + $this->isUpdatingFromLocale = TRUE; $storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode); foreach ($storage->listAll() as $name) { $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); } - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -330,29 +403,234 @@ public function translateString($name, $langcode, $source, $context) { } /** + * Reset static cache of configuration string translations. + * + * @return $this + */ + public function reset() { + $this->translations = array(); + return $this; + } + + /** + * Get the translation object for the given source/context and language. + * + * @param string $name + * Name of the configuration location. + * @param string $langcode + * Language code to translate to. + * @param string $source + * The source string, should be English. + * @param string $context + * The string context. + * + * @return \Drupal\locale\TranslationString|FALSE + * The translation object if the string was not empty or FALSE otherwise. + */ + public function getStringTranslation($name, $langcode, $source, $context) { + if ($source) { + $this->translateString($name, $langcode, $source, $context); + if ($string = $this->translations[$name][$langcode][$context][$source]) { + if (!$string->isTranslation()) { + $conditions = array('lid' => $string->lid, 'language' => $langcode); + return $this->localeStorage->createTranslation($conditions); + } + else { + return $string; + } + } + } + return FALSE; + } + + /** * Checks whether a language has configuration translation. * * @param string $name * Configuration name. - * @param \Drupal\Core\Language\LanguageInterface $language - * A language object. + * @param string $langcode + * A language code. * * @return bool * A boolean indicating if a language has configuration translations. */ - public function hasTranslation($name, LanguageInterface $language) { - $translation = $this->languageManager->getLanguageConfigOverride($language->getId(), $name); + public function hasTranslation($name, $langcode) { + $translation = $this->languageManager->getLanguageConfigOverride($langcode, $name); return !$translation->isNew(); } /** - * Indicates whether configuration translations are currently being updated. + * Returns the original language code for this shipped configuration. + * + * @param string $name + * The configuration name. + * + * @return null|string + * Language code of the default configuration for $name. If the default + * configuration data for $name did not contain a language code, it is + * assumed to be English. The return value is NULL if no such default + * configuration exists. + */ + public function getDefaultConfigLangcode($name) { + $shipped = $this->installStorageRead($name); + if (!empty($shipped)) { + return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en'; + } + } + + /** + * Returns the current language code for this active configuration. + * + * @param string $name + * The configuration name. + * + * @return null|string + * Language code of the current active configuration for $name. If the + * configuration data for $name did not contain a language code, it is + * assumed to be English. The return value is NULL if no such active + * configuration exists. + */ + public function getActiveConfigLangcode($name) { + $active = $this->configStorage->read($name); + if (!empty($active)) { + return !empty($active['langcode']) ? $active['langcode'] : 'en'; + } + } + + /** + * Whether the given configuration is supported for interface translation. + * + * @param string $name + * The configuration name. + * + * @return bool + * TRUE if interface translation is supported. + */ + public function isSupported($name) { + return $this->getDefaultConfigLangcode($name) == 'en' && $this->configStorage->read($name); + } + + /** + * Indicates whether configuration translations are being updated from locale. * * @return bool * Whether or not configuration translations are currently being updated. + * If TRUE, LocaleConfigManager is in control of the process and the + * reference data is locale's storage. Changes made to active configuration + * and overrides in this case should not feed back to locale storage. + * On the other hand, when not updating from locale and configuration + * translations change, we need to feed back to the locale storage. + */ + public function isUpdatingTranslationsFromLocale() { + return $this->isUpdatingFromLocale; + } + + /** + * Updates all configuration translations for the names / languages provided. + * + * To be used when interface translation changes result in the need to update + * configuration translations to keep them in sync. + * + * @param array $names + * Array of names of configuration objects to update. + * @param array $langcodes + * (optional) Array of language codes to update. Defaults to all + * configurable languages. + * + * @return int + * Total number of configuration override and active configuration objects + * updated (saved or removed). + */ + public function updateConfigTranslations(array $names, array $langcodes = array()) { + $langcodes = $langcodes ? $langcodes : array_keys($this->languageManager->getLanguages()); + $count = 0; + foreach ($names as $name) { + $translatable = $this->getTranslatableDefaultConfig($name); + if (empty($translatable)) { + // If there is nothing translatable in this configuration or not + // supported, skip it. + continue; + } + + $active_langcode = $this->getActiveConfigLangcode($name); + $active = $this->configStorage->read($name); + + foreach ($langcodes as $langcode) { + $processed = $this->processTranslatableData($name, $active, $translatable, $langcode); + if ($langcode != $active_langcode) { + // If the language code is not the same as the active storage + // language, we should update a configuration override. + if (!empty($processed)) { + // Update translation data in configuration override. + $this->saveTranslationOverride($name, $langcode, $processed); + $count++; + } + else { + $override = $this->languageManager->getLanguageConfigOverride($langcode, $name); + if (!$override->isNew()) { + $data = $this->filterOverride($override->get(), $translatable); + if (empty($data)) { + // Delete language override if there is no data left at all. + // This means all prior translations in the override were locale + // managed. + $this->deleteTranslationOverride($name, $langcode); + $count++; + } + else { + // If there were translatable elements besides locale managed + // items, save with only those, and remove the ones managed + // by locale only. + $this->saveTranslationOverride($name, $langcode, $data); + $count++; + } + } + } + } + elseif (locale_is_translatable($langcode)) { + // If the language code is the active storage language, we should + // update. If it is English, we should only update if English is also + // translatable. + $active = NestedArray::mergeDeepArray(array($active, $processed), TRUE); + $this->saveTranslationActive($name, $active); + $count++; + } + } + } + return $count; + } + + /** + * Filters override data based on default translatable items. + * + * @param array $override_data + * Configuration override data. + * @param array $translatable + * Translatable data array. @see self::getTranslatableData() + * @return array + * Nested array of any items of $override_data which did not have keys in + * $translatable. May be empty if $override_data only had items which were + * also in $translatable. */ - public function isUpdatingConfigTranslations() { - return $this->isUpdating; + protected function filterOverride(array $override_data, array $translatable) { + $filtered_data = array(); + foreach ($override_data as $key => $value) { + if (isset($translatable[$key])) { + // If the translatable default configuration has this key, look further + // for subkeys or ignore this element for scalar values. + if (is_array($value)) { + $value = $this->filterOverride($value, $translatable[$key]); + if (!empty($value)) { + $filtered_data[$key] = $value; + } + } + } + else { + // If this key was not in the translatable default configuration, + // keep it. + $filtered_data[$key] = $value; + } + } + return $filtered_data; } /** diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php index 6c42d50..ee20daf 100644 --- a/core/modules/locale/src/LocaleConfigSubscriber.php +++ b/core/modules/locale/src/LocaleConfigSubscriber.php @@ -6,35 +6,37 @@ namespace Drupal\locale; -use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigCrudEvent; +use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\TypedData\TraversableTypedDataInterface; -use Drupal\language\Config\LanguageConfigOverride; +use Drupal\Core\Config\StorableConfigBase; use Drupal\language\Config\LanguageConfigOverrideCrudEvent; use Drupal\language\Config\LanguageConfigOverrideEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * Updates corresponding string translation when language overrides change. + * Updates strings translation when configuration translations change. * - * This reacts to the updating or deleting of configuration language overrides. - * It checks whether there are string translations associated with the - * configuration that is being saved and, if so, updates those string - * translations with the new configuration values and marks them as customized. - * That way manual updates to configuration will not be inadvertently reverted - * when updated translations from https://localize.drupal.org are being - * imported. + * This reacts to the updates of translated active configuration and + * configuration language overrides. When those updates involve configuration + * which was available as default configuration, we need to feed back changes + * to any item which was originally part of that configuration to the interface + * translation storage. Those updated translations are saved as customized, so + * further community translation updates will not undo user changes. + * + * This subscriber does not respond to deleting active configuration or deleting + * configuration translations. The locale storage is additive and we cannot be + * sure that only a given configuration translation used a source string. So + * we should not remove the translations from locale storage in these cases. The + * configuration or override would itself be deleted either way. + * + * By design locale module only deals with sources in English. + * + * @see \Drupal\locale\LocaleConfigManager */ class LocaleConfigSubscriber implements EventSubscriberInterface { /** - * The string storage. - * - * @var \Drupal\locale\StringStorageInterface; - */ - protected $stringStorage; - - /** * The configuration factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface @@ -51,15 +53,12 @@ class LocaleConfigSubscriber implements EventSubscriberInterface { /** * Constructs a LocaleConfigSubscriber. * - * @param \Drupal\locale\StringStorageInterface $string_storage - * The string storage. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. * @param \Drupal\locale\LocaleConfigManager $locale_config_manager * The typed configuration manager. */ - public function __construct(StringStorageInterface $string_storage, ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) { - $this->stringStorage = $string_storage; + public function __construct(ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) { $this->configFactory = $config_factory; $this->localeConfigManager = $locale_config_manager; } @@ -68,202 +67,164 @@ public function __construct(StringStorageInterface $string_storage, ConfigFactor * {@inheritdoc} */ public static function getSubscribedEvents() { - $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onSave'; - $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onDelete'; + $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onOverrideChange'; + $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onOverrideChange'; + $events[ConfigEvents::SAVE] = 'onConfigSave'; return $events; } - /** - * Updates the translation strings when shipped configuration is saved. + * Updates the locale strings when a translated active configuration is saved. * - * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event - * The language configuration event. + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The configuration event. */ - public function onSave(LanguageConfigOverrideCrudEvent $event) { - // Do not mark strings as customized when community translations are being - // imported. - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'saveTranslation']; + public function onConfigSave(ConfigCrudEvent $event) { + // Only attempt to feed back configuration translation changes to locale if + // the update itself was not initiated by locale data changes. + if (!$this->localeConfigManager->isUpdatingTranslationsFromLocale()) { + $config = $event->getConfig(); + $langcode = $config->get('langcode') ?: 'en'; + $this->updateLocaleStorage($config, $langcode); } - else { - $callable = [$this, 'saveCustomizedTranslation']; - } - - $this->updateTranslationStrings($event, $callable); } /** - * Updates the translation strings when shipped configuration is deleted. + * Updates the locale strings when a configuration override is saved/deleted. * * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event * The language configuration event. */ - public function onDelete(LanguageConfigOverrideCrudEvent $event) { - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'deleteTranslation']; - } - else { - // Do not delete the string, but save a customized translation with the - // source value so that the deletion will not be reverted by importing - // community translations. - // @see \Drupal\locale\LocaleConfigSubscriber::saveCustomizedTranslation() - $callable = [$this, 'saveCustomizedTranslation']; + public function onOverrideChange(LanguageConfigOverrideCrudEvent $event) { + // Only attempt to feed back configuration override changes to locale if + // the update itself was not initiated by locale data changes. + if (!$this->localeConfigManager->isUpdatingTranslationsFromLocale()) { + $translation_config = $event->getLanguageConfigOverride(); + $langcode = $translation_config->getLangcode(); + $reference_config = $this->configFactory->getEditable($translation_config->getName())->get(); + $this->updateLocaleStorage($translation_config, $langcode, $reference_config); } - - $this->updateTranslationStrings($event, $callable); } /** - * Updates the translation strings of shipped configuration. + * Update locale storage based on configuration translations. * - * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event - * The language configuration event. - * @param $callable - * A callable to apply to each translatable string of the configuration. + * @param \Drupal\Core\Config\StorableConfigBase $config + * Active configuration or configuration translation override. + * @param string $langcode + * The language code of $config. + * @param array $reference_config + * (Optional) Reference configuration to check against if $config was an + * override. This allows us to update locale keys for data not in the + * override but still in the active configuration. */ - protected function updateTranslationStrings(LanguageConfigOverrideCrudEvent $event, $callable) { - $translation_config = $event->getLanguageConfigOverride(); - $name = $translation_config->getName(); - - // Only do anything if the configuration was shipped. - if ($this->stringStorage->getLocations(['type' => 'configuration', 'name' => $name])) { - $source_config = $this->configFactory->getEditable($name); - $schema = $this->localeConfigManager->get($name)->getTypedConfig(); - $this->traverseSchema($schema, $source_config, $translation_config, $callable); + protected function updateLocaleStorage(StorableConfigBase $config, $langcode, array $reference_config = array()) { + $name = $config->getName(); + if ($this->localeConfigManager->isSupported($name) && locale_is_translatable($langcode)) { + $translatables = $this->localeConfigManager->getTranslatableDefaultConfig($name); + $this->processTranslatableData($name, $config->get(), $translatables, $langcode, $reference_config); } } /** - * Traverses configuration schema and applies a callback to each leaf element. - * - * It skips leaf elements that are not translatable. - * - * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $schema - * The respective configuration schema. - * @param callable $callable - * The callable to apply to each leaf element. The callable will be called - * with the leaf element and the element key as arguments. - * @param string|null $base_key - * (optional) The base key that the schema belongs to. This should be NULL - * for the top-level schema and be populated consecutively when recursing - * into the schema structure. + * Process the translatable data array with a given language. + * + * @param string $name + * The configuration name. + * @param array $config + * The active configuration data or override data. + * @param array|\Drupal\Core\StringTranslation\TranslationWrapper[] $translatable + * The translatable array structure. + * @see \Drupal\locale\LocaleConfigManager::getTranslatableData() + * @param string $langcode + * The language code to process the array with. + * @param array $reference_config + * (Optional) Reference configuration to check against if $config was an + * override. This allows us to update locale keys for data not in the + * override but still in the active configuration. */ - protected function traverseSchema(TraversableTypedDataInterface $schema, Config $source_config, LanguageConfigOverride $translation_config, $callable, $base_key = NULL) { - foreach ($schema as $key => $element) { - $element_key = implode('.', array_filter([$base_key, $key])); - - // We only care for strings here, so traverse the schema further in the - // case of traversable elements. - if ($element instanceof TraversableTypedDataInterface) { - $this->traverseSchema($element, $source_config, $translation_config, $callable, $element_key); + protected function processTranslatableData($name, array $config, array $translatable, $langcode, array $reference_config = array()) { + foreach ($translatable as $key => $item) { + if (!isset($config[$key])) { + if (isset($reference_config[$key])) { + $this->resetExistingTranslations($name, $translatable[$key], $reference_config[$key], $langcode); + } + continue; + } + if (is_array($item)) { + $reference_config = isset($reference_config[$key]) ? $reference_config[$key] : array(); + $this->processTranslatableData($name, $config[$key], $item, $langcode, $reference_config); } - // Skip elements which are not translatable. - elseif (!empty($element->getDataDefinition()['translatable'])) { - $callable( - $source_config->get($element_key), - $translation_config->getLangcode(), - $translation_config->get($element_key) - ); + else { + $this->saveCustomizedTranslation($name, $item->getUntranslatedString(), $item->getOption('context'), $config[$key], $langcode); } } } /** - * Saves a translation string. - * - * @param string $source_value - * The source string value. + * Reset existing locale translations to their source values. + * + * Goes through $translatable to reset any existing translations to the source + * string, so prior translations would not reappear in the configuration. + * + * @param string $name + * The configuration name. + * @param array|\Drupal\Core\StringTranslation\TranslationWrapper $translatable + * Either a possibly nested array with TranslationWrapper objects at the + * leaf items or a TranslationWrapper object directly. + * @param array|string $reference_config + * Either a possibly nested array with strings at the leaf items or a string + * directly. Only those $translatable items that are also present in + * $reference_config will get translations reset. * @param string $langcode - * The language code of the translation. - * @param string|null $translation_value - * (optional) The translation string value. If omitted, no translation will - * be saved. + * The language code of the translation being processed. */ - protected function saveTranslation($source_value, $langcode, $translation_value = NULL) { - if ($translation_value && ($translation = $this->getTranslation($source_value, $langcode, TRUE))) { - if ($translation->isNew() || $translation->getString() != $translation_value) { - $translation - ->setString($translation_value) - ->save(); + protected function resetExistingTranslations($name, $translatable, $reference_config, $langcode) { + if (is_array($translatable)) { + foreach ($translatable as $key => $item) { + if (isset($reference_config[$key])) { + // Process further if the key still exists in the reference active + // configuration and the default translation but not the current + // configuration override. + $this->resetExistingTranslations($name, $item, $reference_config[$key], $langcode); + } } } + elseif (!is_array($reference_config)) { + $this->saveCustomizedTranslation($name, $translatable->getUntranslatedString(), $translatable->getOption('context'), $reference_config, $langcode); + } } /** * Saves a translation string and marks it as customized. * - * @param string $source_value + * @param string $name + * The configuration name. + * @param string $source * The source string value. + * @param string $context + * The source string context. + * @param string $translation + * The translation string. * @param string $langcode * The language code of the translation. - * @param string|null $translation_value - * (optional) The translation string value. If omitted, a customized string - * with the source value will be saved. - * - * @see \Drupal\locale\LocaleConfigSubscriber::onDelete() */ - protected function saveCustomizedTranslation($source_value, $langcode, $translation_value = NULL) { - if ($translation = $this->getTranslation($source_value, $langcode, TRUE)) { - if (!isset($translation_value)) { - $translation_value = $source_value; - } - if ($translation->isNew() || $translation->getString() != $translation_value) { - $translation - ->setString($translation_value) + protected function saveCustomizedTranslation($name, $source, $context, $translation, $langcode) { + $locale_translation = $this->localeConfigManager->getStringTranslation($name, $langcode, $source, $context); + if (!empty($locale_translation)) { + // Save this translation as custom if it was a new translation and not the + // same as the source. (The interface prefills translation values with the + // source). Or if there was an existing translation and the user changed + // it (even if it was changed back to the original value). Otherwise the + // translation file would be overwritten with the locale copy again later. + if (($locale_translation->isNew() && $source != $translation) || + (!$locale_translation->isNew() && $translation != $locale_translation->getString())) { + $locale_translation + ->setString($translation) ->setCustomized(TRUE) ->save(); } } } - /** - * Deletes a translation string, if it exists. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * - * @see \Drupal\locale\LocaleConfigSubscriber::onDelete() - */ - protected function deleteTranslation($source_value, $langcode) { - if ($translation = $this->getTranslation($source_value, $langcode, FALSE)) { - $translation->delete(); - } - } - - /** - * Gets a translation string. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * @param bool $create_fallback - * (optional) By default if a source string could be found and no - * translation in the given language exists yet, a translation object is - * created. This can be circumvented by passing FALSE. - * - * @return \Drupal\locale\TranslationString|null - * The translation string if one was found or created. - */ - protected function getTranslation($source_value, $langcode, $create_fallback = TRUE) { - // There is no point in creating a translation without a source. - if ($source_string = $this->stringStorage->findString(['source' => $source_value])) { - // Get the translation for this original source string from locale. - $conditions = [ - 'lid' => $source_string->lid, - 'language' => $langcode, - ]; - $translations = $this->stringStorage->getTranslations($conditions + ['translated' => TRUE]); - if ($translations) { - return reset($translations); - } - elseif ($create_fallback) { - return $this->stringStorage->createTranslation($conditions); - } - } - } - } diff --git a/core/modules/locale/src/LocaleTypedConfig.php b/core/modules/locale/src/LocaleTypedConfig.php deleted file mode 100644 index ca31a38..0000000 --- a/core/modules/locale/src/LocaleTypedConfig.php +++ /dev/null @@ -1,209 +0,0 @@ -langcode = $langcode; - $this->localeConfig = $locale_config; - $this->typedConfigManager = $typed_config; - $this->languageManager = $language_manager; - } - - /** - * Gets wrapped typed config object. - */ - public function getTypedConfig() { - return $this->typedConfigManager->create($this->definition, $this->value); - } - - /** - * {@inheritdoc} - */ - public function getTranslation($langcode) { - $options = array( - 'source' => $this->langcode, - 'target' => $langcode, - ); - $data = $this->getElementTranslation($this->getTypedConfig(), $options); - return $this->typedConfigManager->create($this->definition, $data); - } - - /** - * {@inheritdoc} - */ - public function language() { - return $this->languageManager->getLanguage($this->langcode); - } - - /** - * Checks whether we can translate these languages. - * - * @param string $from_langcode - * Source language code. - * @param string $to_langcode - * Destination language code. - * - * @return bool - * TRUE if this translator supports translations for these languages. - */ - protected function canTranslate($from_langcode, $to_langcode) { - if ($from_langcode == 'en') { - return TRUE; - } - return FALSE; - } - - /** - * Gets translated configuration data for a typed configuration element. - * - * @param \Drupal\Core\TypedData\TypedDataInterface $element - * Typed configuration element. - * @param array $options - * Array with translation options that must contain the keys defined in - * \Drupal\locale\LocaleTypedConfig::translateElement(). - * - * @return array - * Configuration data translated to the requested language if available, - * an empty array otherwise. - */ - protected function getElementTranslation(TypedDataInterface $element, array $options) { - $translation = array(); - if ($element instanceof TraversableTypedDataInterface) { - $translation = $this->getArrayTranslation($element, $options); - } - elseif ($this->translateElement($element, $options)) { - $translation = $element->getValue(); - } - return $translation; - } - - /** - * Gets translated configuration data for a traversable element. - * - * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $element - * Typed configuration array element. - * @param array $options - * Array with translation options that must contain the keys defined in - * \Drupal\locale\LocaleTypedConfig::translateElement(). - * - * @return array - * Configuration data translated to the requested language. - */ - protected function getArrayTranslation(TraversableTypedDataInterface $element, array $options) { - $translation = array(); - foreach ($element as $key => $property) { - $value = $this->getElementTranslation($property, $options); - if (!empty($value)) { - $translation[$key] = $value; - } - } - return $translation; - } - - /** - * Translates element's value if it fits our translation criteria. - * - * For an element to be translatable by locale module it needs to be of base - * type 'string' and have 'translatable = TRUE' in the element's definition. - * Translatable elements may use these additional keys in their data - * definition: - * - 'translatable', FALSE to opt out of translation. - * - 'translation context', to define the string context. - * - * @param \Drupal\Core\TypedData\TypedDataInterface $element - * Configuration element. - * @param array $options - * Array with translation options that must contain the following keys: - * - 'source', Source language code. - * - 'target', Target language code. - * - * @return bool - * Whether the element fits the translation criteria. - */ - protected function translateElement(TypedDataInterface $element, array $options) { - if ($this->canTranslate($options['source'], $options['target'])) { - $definition = $element->getDataDefinition(); - $value = $element->getValue(); - if ($value && !empty($definition['translatable'])) { - $context = isset($definition['translation context']) ? $definition['translation context'] : ''; - if ($translation = $this->localeConfig->translateString($this->name, $options['target'], $value, $context)) { - $element->setValue($translation); - return TRUE; - } - } - } - // The element does not have a translation. - return FALSE; - } - -} diff --git a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php index 4cc9fb2..58d63cc 100644 --- a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php @@ -28,16 +28,16 @@ class LocaleConfigManagerTest extends KernelTestBase { * Tests hasTranslation(). */ public function testHasTranslation() { - $this->installSchema('locale', array('locales_location')); + $this->installSchema('locale', array('locales_location', 'locales_source', 'locales_target')); $this->installConfig(array('locale_test')); - $locale_config_manager = \Drupal::service('locale.config.typed'); + $locale_config_manager = \Drupal::service('locale.config_manager'); $language = ConfigurableLanguage::createFromLangcode('de'); $language->save(); - $result = $locale_config_manager->hasTranslation('locale_test.no_translation', $language); + $result = $locale_config_manager->hasTranslation('locale_test.no_translation', $language->getId()); $this->assertFalse($result, 'There is no translation for locale_test.no_translation configuration.'); - $result = $locale_config_manager->hasTranslation('locale_test.translation', $language); + $result = $locale_config_manager->hasTranslation('locale_test.translation', $language->getId()); $this->assertTrue($result, 'There is a translation for locale_test.translation configuration.'); } } diff --git a/core/modules/locale/src/Tests/LocaleConfigSubscriberForeignTest.php b/core/modules/locale/src/Tests/LocaleConfigSubscriberForeignTest.php new file mode 100644 index 0000000..00018f1 --- /dev/null +++ b/core/modules/locale/src/Tests/LocaleConfigSubscriberForeignTest.php @@ -0,0 +1,170 @@ +save(); + } + + /** + * {@inheritdoc} + */ + protected function setUpLocale() { + parent::setUpLocale(); + $this->setUpTranslation('locale_test.translation', 'test', 'English test', 'Hungarian test', 'hu', TRUE); + } + + /** + * Tests that the language of default configuration was updated. + */ + public function testDefaultConfigLanguage() { + $this->assertEqual('hu', $this->configFactory->getEditable('locale_test.no_translation')->get('langcode')); + $this->assertEqual('hu', $this->configFactory->getEditable('locale_test.translation')->get('langcode')); + $this->assertEqual($this->configFactory->getEditable('locale_test.translation')->get('test'), 'Hungarian test'); + } + + /** + * Tests creating translations of shipped configuration. + */ + public function testCreateActiveTranslation() { + $config_name = 'locale_test.no_translation'; + $this->saveLanguageActive($config_name, 'test', 'Test (Hungarian)', 'hu'); + $this->assertTranslation($config_name, 'Test (Hungarian)', 'hu'); + } + + /** + * Tests importing community translations of shipped configuration. + */ + public function testLocaleCreateActiveTranslation() { + $config_name = 'locale_test.no_translation'; + $this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (Hungarian)', 'hu', TRUE); + $this->assertTranslation($config_name, 'Test (Hungarian)', 'hu', FALSE); + } + + /** + * Tests updating translations of shipped configuration. + */ + public function testUpdateActiveTranslation() { + $config_name = 'locale_test.translation'; + $this->saveLanguageActive($config_name, 'test', 'Updated Hungarian test', 'hu'); + $this->assertTranslation($config_name, 'Updated Hungarian test', 'hu'); + } + + /** + * Tests updating community translations of shipped configuration. + */ + public function testLocaleUpdateActiveTranslation() { + $config_name = 'locale_test.translation'; + $this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated Hungarian test', 'hu', TRUE); + $this->assertTranslation($config_name, 'Updated Hungarian test', 'hu', FALSE); + } + + /** + * Tests deleting a translation override. + */ + public function testDeleteTranslation() { + $config_name = 'locale_test.translation'; + $this->deleteLanguageOverride($config_name, 'test', 'English test', 'de'); + // The German translation in this case will be forced to the Hungarian + // source so its not overwritten with locale data later. + $this->assertTranslation($config_name, 'Hungarian test', 'de'); + } + + /** + * Tests deleting translations of shipped configuration. + */ + public function testDeleteActiveTranslation() { + $config_name = 'locale_test.translation'; + $this->configFactory->getEditable($config_name)->delete(); + // Deleting active configuration should not change the locale translation. + $this->assertTranslation($config_name, 'Hungarian test', 'hu', FALSE); + } + + /** + * Tests deleting community translations of shipped configuration. + */ + public function testLocaleDeleteActiveTranslation() { + $config_name = 'locale_test.translation'; + $this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'hu'); + // Deleting the locale translation should not change active config. + $this->assertEqual($this->configFactory->getEditable($config_name)->get('test'), 'Hungarian test'); + } + + /** + * Tests that adding English creates a translation override. + */ + public function testEnglish() { + $config_name = 'locale_test.translation'; + ConfigurableLanguage::createFromLangcode('en')->save(); + // Adding a language on the UI would normally call updateConfigTranslations. + $this->localeConfigManager->updateConfigTranslations(array($config_name), array('en')); + $this->assertConfigOverride($config_name, 'test', 'English test', 'en'); + + $this->configFactory->getEditable('locale.settings')->set('translate_english', TRUE)->save(); + $this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated English test', 'en'); + $this->assertTranslation($config_name, 'Updated English test', 'en', FALSE); + + $this->saveLanguageOverride($config_name, 'test', 'Updated English', 'en'); + $this->assertTranslation($config_name, 'Updated English', 'en'); + + $this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'en'); + $this->assertNoConfigOverride($config_name, 'en'); + } + + /** + * Saves a language override. + * + * This will invoke LocaleConfigSubscriber through the event dispatcher. To + * make sure the configuration was persisted correctly, the configuration + * value is checked. Because LocaleConfigSubscriber temporarily disables the + * override state of the configuration factory we check that the correct value + * is restored afterwards. + * + * @param string $config_name + * The configuration name. + * @param string $key + * The configuration key. + * @param string $value + * The configuration value to save. + * @param string $langcode + * The language code. + */ + protected function saveLanguageActive($config_name, $key, $value, $langcode) { + $this + ->configFactory + ->getEditable($config_name) + ->set($key, $value) + ->save(); + $this->assertActiveConfig($config_name, $key, $value, $langcode); + } + +} diff --git a/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php index f3f7415..24017fd 100644 --- a/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php @@ -22,7 +22,7 @@ class LocaleConfigSubscriberTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['language', 'locale', 'locale_test']; + public static $modules = ['language', 'locale']; /** * The configurable language manager used in this test. @@ -53,27 +53,52 @@ class LocaleConfigSubscriberTest extends KernelTestBase { protected $localeConfigManager; /** - * The language code used in this test. - * - * @var string - */ - protected $langcode = 'de'; - - /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); - $this->languageManager = $this->container->get('language_manager'); - $this->configFactory = $this->container->get('config.factory'); - $this->stringStorage = $this->container->get('locale.storage'); - $this->localeConfigManager = $this->container->get('locale.config.typed'); + $this->setUpDefaultLanguage(); $this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']); + $this->setupLanguages(); + + $this->enableModules(['locale_test']); $this->installConfig(['locale_test']); - ConfigurableLanguage::createFromLangcode($this->langcode)->save(); + // Simulate this hook invoked which would happen if in a non-kernel test + // or normal environment. + locale_modules_installed(array('locale_test')); + + $this->configFactory = $this->container->get('config.factory'); + $this->stringStorage = $this->container->get('locale.storage'); + $this->localeConfigManager = $this->container->get('locale.config_manager'); + $this->languageManager = $this->container->get('language_manager'); + + $this->setUpLocale(); + } + + /** + * Sets up default language for this test. + */ + protected function setUpDefaultLanguage() { + // Keep the default English. + } + + /** + * Sets up languages needed for this test. + */ + protected function setUpLanguages() { + ConfigurableLanguage::createFromLangcode('de')->save(); + } + + /** + * Sets up the locale storage strings to be in line with configuration. + */ + protected function setUpLocale() { + // Set up the locale database the same way we have in the config samples. + $this->setUpNoTranslation('locale_test.no_translation', 'test', 'Test', 'de'); + $this->setUpTranslation('locale_test.translation', 'test', 'English test', 'German test', 'de'); } /** @@ -82,9 +107,8 @@ protected function setUp() { public function testCreateTranslation() { $config_name = 'locale_test.no_translation'; - $this->setUpNoTranslation($config_name, 'test', 'Test'); - $this->saveLanguageOverride($config_name, 'test', 'Test (German)'); - $this->assertTranslation($config_name, 'Test (German)'); + $this->saveLanguageOverride($config_name, 'test', 'Test (German)', 'de'); + $this->assertTranslation($config_name, 'Test (German)', 'de'); } /** @@ -93,9 +117,8 @@ public function testCreateTranslation() { public function testLocaleCreateTranslation() { $config_name = 'locale_test.no_translation'; - $this->setUpNoTranslation($config_name, 'test', 'Test'); - $this->saveLocaleTranslationData($config_name, 'test', 'Test (German)'); - $this->assertTranslation($config_name, 'Test (German)', FALSE); + $this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (German)', 'de'); + $this->assertTranslation($config_name, 'Test (German)', 'de', FALSE); } /** @@ -104,9 +127,8 @@ public function testLocaleCreateTranslation() { public function testUpdateTranslation() { $config_name = 'locale_test.translation'; - $this->setUpTranslation($config_name, 'test', 'English test', 'German test'); - $this->saveLanguageOverride($config_name, 'test', 'Updated German test'); - $this->assertTranslation($config_name, 'Updated German test'); + $this->saveLanguageOverride($config_name, 'test', 'Updated German test', 'de'); + $this->assertTranslation($config_name, 'Updated German test', 'de'); } /** @@ -115,9 +137,8 @@ public function testUpdateTranslation() { public function testLocaleUpdateTranslation() { $config_name = 'locale_test.translation'; - $this->setUpTranslation($config_name, 'test', 'English test', 'German test'); - $this->saveLocaleTranslationData($config_name, 'test', 'Updated German test'); - $this->assertTranslation($config_name, 'Updated German test', FALSE); + $this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated German test', 'de'); + $this->assertTranslation($config_name, 'Updated German test', 'de', FALSE); } /** @@ -126,12 +147,11 @@ public function testLocaleUpdateTranslation() { public function testDeleteTranslation() { $config_name = 'locale_test.translation'; - $this->setUpTranslation($config_name, 'test', 'English test', 'German test'); - $this->deleteLanguageOverride($config_name, 'test', 'English test'); + $this->deleteLanguageOverride($config_name, 'test', 'English test', 'de'); // Instead of deleting the translation, we need to keep a translation with // the source value and mark it as customized to prevent the deletion being // reverted by importing community translations. - $this->assertTranslation($config_name, 'English test'); + $this->assertTranslation($config_name, 'English test', 'de'); } /** @@ -140,9 +160,8 @@ public function testDeleteTranslation() { public function testLocaleDeleteTranslation() { $config_name = 'locale_test.translation'; - $this->setUpTranslation($config_name, 'test', 'English test', 'German test'); - $this->deleteLocaleTranslationData($config_name, 'test', 'English test'); - $this->assertNoTranslation($config_name, 'English test', FALSE); + $this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'de'); + $this->assertNoTranslation($config_name, 'de'); } /** @@ -159,16 +178,13 @@ public function testLocaleDeleteTranslation() { * The configuration key. * @param string $source * The source string. + * @param string $langcode + * The language code. */ - protected function setUpNoTranslation($config_name, $key, $source) { - // Add a source string with the configuration name as a location. This gets - // called from locale_config_update_multiple() normally. - $this->localeConfigManager->translateString($config_name, $this->langcode, $source, ''); - $this->languageManager - ->setConfigOverrideLanguage(ConfigurableLanguage::load($this->langcode)); - - $this->assertConfigValue($config_name, $key, $source); - $this->assertNoTranslation($config_name); + protected function setUpNoTranslation($config_name, $key, $source, $langcode) { + $this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode)); + $this->assertNoConfigOverride($config_name, $key, $source, $langcode); + $this->assertNoTranslation($config_name, $langcode); } @@ -188,27 +204,33 @@ protected function setUpNoTranslation($config_name, $key, $source) { * The source string. * @param string $translation * The translation string. + * @param string $langcode + * The language code. + * @param bool $is_active + * Whether the update will affect the active configuration. */ - protected function setUpTranslation($config_name, $key, $source, $translation) { + protected function setUpTranslation($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) { // Create source and translation strings for the configuration value and add // the configuration name as a location. This would be performed by - // locale_translate_batch_import() and locale_config_update_multiple() - // normally. - $source_object = $this->stringStorage->createString([ - 'source' => $source, - 'context' => '', - ])->save(); - $this->stringStorage->createTranslation([ - 'lid' => $source_object->getId(), - 'language' => $this->langcode, - 'translation' => $translation, - ])->save(); - $this->localeConfigManager->translateString($config_name, $this->langcode, $source, ''); - $this->languageManager - ->setConfigOverrideLanguage(ConfigurableLanguage::load($this->langcode)); - - $this->assertConfigValue($config_name, $key, $translation); - $this->assertTranslation($config_name, $translation, FALSE); + // locale_translate_batch_import() invoking + // LocaleConfigManager::updateConfigTranslations() normally. + $this->localeConfigManager->reset(); + $this->localeConfigManager + ->getStringTranslation($config_name, $langcode, $source, '') + ->setString($translation) + ->setCustomized(FALSE) + ->save(); + $this->configFactory->reset($config_name); + $this->localeConfigManager->reset(); + $this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode)); + + if ($is_active) { + $this->assertActiveConfig($config_name, $key, $translation, $langcode); + } + else { + $this->assertConfigOverride($config_name, $key, $translation, $langcode); + } + $this->assertTranslation($config_name, $translation, $langcode, FALSE); } /** @@ -226,16 +248,18 @@ protected function setUpTranslation($config_name, $key, $source, $translation) { * The configuration key. * @param string $value * The configuration value to save. + * @param string $langcode + * The language code. */ - protected function saveLanguageOverride($config_name, $key, $value) { + protected function saveLanguageOverride($config_name, $key, $value, $langcode) { $translation_override = $this->languageManager - ->getLanguageConfigOverride($this->langcode, $config_name); + ->getLanguageConfigOverride($langcode, $config_name); $translation_override ->set($key, $value) ->save(); $this->configFactory->reset($config_name); - $this->assertConfigValue($config_name, $key, $value); + $this->assertConfigOverride($config_name, $key, $value, $langcode); } /** @@ -251,15 +275,31 @@ protected function saveLanguageOverride($config_name, $key, $value) { * The configuration name. * @param string $key * The configuration key. - * @param string $value - * The configuration value to save. + * @param string $source + * The source string. + * @param string $translation + * The translation string to save. + * @param string $langcode + * The language code. + * @param bool $is_active + * Whether the update will affect the active configuration. */ - protected function saveLocaleTranslationData($config_name, $key, $value) { + protected function saveLocaleTranslationData($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) { + $this->localeConfigManager->reset(); $this->localeConfigManager - ->saveTranslationData($config_name, $this->langcode, [$key => $value]); + ->getStringTranslation($config_name, $langcode, $source, '') + ->setString($translation) + ->save(); + $this->localeConfigManager->reset(); + $this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode)); $this->configFactory->reset($config_name); - $this->assertConfigValue($config_name, $key, $value); + if ($is_active) { + $this->assertActiveConfig($config_name, $key, $translation, $langcode); + } + else { + $this->assertConfigOverride($config_name, $key, $translation, $langcode); + } } /** @@ -278,16 +318,18 @@ protected function saveLocaleTranslationData($config_name, $key, $value) { * @param string $source_value * The source configuration value to verify the correct value is returned * from the configuration factory after the deletion. + * @param string $langcode + * The language code. */ - protected function deleteLanguageOverride($config_name, $key, $source_value) { + protected function deleteLanguageOverride($config_name, $key, $source_value, $langcode) { $translation_override = $this->languageManager - ->getLanguageConfigOverride($this->langcode, $config_name); + ->getLanguageConfigOverride($langcode, $config_name); $translation_override ->clear($key) ->save(); $this->configFactory->reset($config_name); - $this->assertConfigValue($config_name, $key, $source_value); + $this->assertNoConfigOverride($config_name, $key, $source_value, $langcode); } /** @@ -306,31 +348,78 @@ protected function deleteLanguageOverride($config_name, $key, $source_value) { * @param string $source_value * The source configuration value to verify the correct value is returned * from the configuration factory after the deletion. + * @param string $langcode + * The language code. */ - protected function deleteLocaleTranslationData($config_name, $key, $source_value) { - $this->localeConfigManager->deleteTranslationData($config_name, $this->langcode); + protected function deleteLocaleTranslationData($config_name, $key, $source_value, $langcode) { + $this->localeConfigManager + ->getStringTranslation($config_name, $langcode, $source_value, '') + ->delete(); + $this->localeConfigManager->reset(); + $this->localeConfigManager->updateConfigTranslations(array($config_name), array($langcode)); $this->configFactory->reset($config_name); - $this->assertConfigValue($config_name, $key, $source_value); + $this->assertNoConfigOverride($config_name, $key, $source_value, $langcode); + } + + /** + * Ensures configuration override is not present anymore. + * + * @param string $config_name + * The configuration name. + * @param string $langcode + * The language code. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNoConfigOverride($config_name, $langcode) { + $config_langcode = $this->configFactory->getEditable($config_name)->get('langcode'); + $override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name); + return $this->assertNotEqual($config_langcode, $langcode) && $this->assertEqual($override->isNew(), TRUE); + } + + /** + * Ensures configuration was saved correctly. + * + * @param string $config_name + * The configuration name. + * @param string $key + * The configuration key. + * @param string $value + * The configuration value. + * @param string $langcode + * The language code. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertConfigOverride($config_name, $key, $value, $langcode) { + $config_langcode = $this->configFactory->getEditable($config_name)->get('langcode'); + $override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name); + return $this->assertNotEqual($config_langcode, $langcode) && $this->assertEqual($override->get($key), $value); } /** * Ensures configuration was saved correctly. * - * @param $config_name + * @param string $config_name * The configuration name. - * @param $key + * @param string $key * The configuration key. - * @param $value + * @param string $value * The configuration value. + * @param string $langcode + * The language code. * * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ - protected function assertConfigValue($config_name, $key, $value) { - // Make sure the configuration was translated correctly. - $translation_config = $this->configFactory->get($config_name); - return $this->assertIdentical($value, $translation_config->get($key)); + protected function assertActiveConfig($config_name, $key, $value, $langcode) { + $config = $this->configFactory->getEditable($config_name); + return + $this->assertEqual($config->get('langcode'), $langcode) && + $this->assertIdentical($config->get($key), $value); } /** @@ -338,15 +427,17 @@ protected function assertConfigValue($config_name, $key, $value) { * * @param string $config_name * The configuration name. + * @param string $langcode + * The language code. * * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ - protected function assertNoTranslation($config_name) { + protected function assertNoTranslation($config_name, $langcode) { $strings = $this->stringStorage->getTranslations([ 'type' => 'configuration', 'name' => $config_name, - 'language' => $this->langcode, + 'language' => $langcode, 'translated' => TRUE, ]); return $this->assertIdentical([], $strings); @@ -359,6 +450,8 @@ protected function assertNoTranslation($config_name) { * The configuration name. * @param string $translation * The translation. + * @param string $langcode + * The language code. * @param bool $customized * Whether or not the string should be asserted to be customized or not * customized. @@ -366,12 +459,12 @@ protected function assertNoTranslation($config_name) { * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ - protected function assertTranslation($config_name, $translation, $customized = TRUE) { + protected function assertTranslation($config_name, $translation, $langcode, $customized = TRUE) { // Make sure a string exists. $strings = $this->stringStorage->getTranslations([ 'type' => 'configuration', 'name' => $config_name, - 'language' => $this->langcode, + 'language' => $langcode, 'translated' => TRUE, ]); $pass = $this->assertIdentical(1, count($strings)); diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php index 27f120d..d5a7c21 100644 --- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php @@ -79,12 +79,10 @@ public function testConfigTranslation() { ); $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations')); - $wrapper = $this->container->get('locale.config.typed')->get('system.site'); - // Get translation and check we've only got the site name. - $translation = $wrapper->getTranslation($langcode); - $properties = $translation->getElements(); - $this->assertEqual(count($properties), 1, 'Got the right number of properties after translation'); + $translation = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'system.site')->get(); + $this->assertEqual(count($translation), 1, 'Got the right number of properties after translation.'); + $this->assertEqual($translation['name'], $site_name, 'Got the right translation for the site name.'); // Check the translated site name is displayed. $this->drupalGet($langcode); @@ -109,12 +107,8 @@ public function testConfigTranslation() { ); $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations')); - $wrapper = $this->container->get('locale.config.typed')->get('core.date_format.medium'); - - // Get translation and check we've only got the site name. - $translation = $wrapper->getTranslation($langcode); - $format = $translation->get('pattern')->getValue(); - $this->assertEqual($format, 'D', 'Got the right date format pattern after translation.'); + $translation = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'core.date_format.medium')->get(); + $this->assertEqual($translation['pattern'], 'D', 'Got the right date format pattern after translation.'); // Formatting the date 8 / 27 / 1985 @ 13:37 EST with pattern D should // display "Tue". @@ -162,15 +156,8 @@ public function testConfigTranslation() { $this->assertTrue(count($translations) == 1 && $translation->source == $string->source && $translation->translation == $image_style_label, 'Got only one translation for image configuration.'); // Try more complex configuration data. - $wrapper = $this->container->get('locale.config.typed')->get('image.style.medium'); - - $translation = $wrapper->getTranslation($langcode); - $property = $translation->get('label'); - $this->assertEqual($property->getValue(), $image_style_label, 'Got the right translation for image style name after translation'); - - // Quick test to ensure translation file exists. - $override = \Drupal::languageManager()->getLanguageConfigOverride('xx', 'image.style.medium'); - $this->assertEqual($override->get('label'), $image_style_label); + $translation = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'image.style.medium')->get(); + $this->assertEqual($translation['label'], $image_style_label, 'Got the right translation for image style name after translation'); // Uninstall the module. $this->drupalPostForm('admin/modules/uninstall', array('uninstall[image]' => "image"), t('Uninstall')); diff --git a/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml b/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml index 5e7e056..daf3391 100644 --- a/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml +++ b/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml @@ -9,6 +9,8 @@ locale_test.no_translation: label: 'Test' # See \Drupal\locale\Tests\LocaleConfigSubscriberTest translatable: true + langcode: + type: string locale_test.translation: type: mapping @@ -19,3 +21,5 @@ locale_test.translation: label: 'Test' # See \Drupal\locale\Tests\LocaleConfigSubscriberTest translatable: true + langcode: + type: string diff --git a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php index 085722f..fe5e65a 100644 --- a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php +++ b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php @@ -134,34 +134,4 @@ function testMenuLanguage() { $this->assertNoField('edit-langcode-0-value', 'The language selector field was hidden the page'); } - /** - * Tests menu configuration is still English after English has been deleted. - */ - function testMenuLanguageRemovedEnglish() { - // Create a test menu to test language settings. - // Machine name has to be lowercase. - $menu_name = Unicode::strtolower($this->randomMachineName(16)); - $edit = array( - 'id' => $menu_name, - 'description' => '', - 'label' => $this->randomString(), - 'langcode' => 'en', - ); - $this->drupalPostForm('admin/structure/menu/add', $edit, t('Save')); - - // Check that the language settings were saved. - $menu = Menu::load($menu_name); - $this->assertEqual($menu->language()->getId(), 'en'); - - // Remove English language. To do that another language has to be set as - // default. - $this->config('system.site')->set('default_langcode', 'cs')->save(); - entity_delete_multiple('configurable_language', array('en')); - - // Save the menu again and check if the language is still the same. - $this->drupalPostForm("admin/structure/menu/manage/$menu_name", array(), t('Save')); - $menu = Menu::load($menu_name); - $this->assertEqual($menu->language()->getId(), 'en'); - } - } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index abc1f20..c4d94eb 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -290,7 +290,7 @@ public function containerBuild(ContainerBuilder $container) { $this->container = $container; // Set the default language on the minimal container. - $this->container->setParameter('language.default_values', Language::$defaultValues); + $this->container->setParameter('language.default_values', $this->defaultLanguageData()); $container->register('lock', 'Drupal\Core\Lock\NullLockBackend'); $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); @@ -359,6 +359,16 @@ public function containerBuild(ContainerBuilder $container) { } /** + * Provides the data for setting the default language on the container. + * + * @return array + * The data array for the default language. + */ + protected function defaultLanguageData() { + return Language::$defaultValues; + } + + /** * Installs default configuration for a given list of modules. * * @param array $modules diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageForeignTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageForeignTest.php index 248d485..918e85e 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageForeignTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageForeignTest.php @@ -26,7 +26,7 @@ class InstallerTranslationMultipleLanguageForeignTest extends InstallerTranslati */ protected function setUpLanguage() { parent::setUpLanguage(); - $this->translations['Save and continue'] = 'Save and continue German'; + $this->translations['Save and continue'] = 'Save and continue de'; } } diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageKeepEnglishTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageKeepEnglishTest.php index 0c01fe6..79085db 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageKeepEnglishTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageKeepEnglishTest.php @@ -2,51 +2,23 @@ /** * @file - * Contains \Drupal\system\Tests\Installer\InstallerTranslationMultipleLanguageForeignTest. + * Contains \Drupal\system\Tests\Installer\InstallerTranslationMultipleLanguageKeepEnglishTest. */ namespace Drupal\system\Tests\Installer; -use Drupal\simpletest\InstallerTestBase; /** - * Tests translation files for multiple languages get imported during install. + * Tests that keeping English in a foreign language install works. * * @group Installer */ -class InstallerTranslationMultipleLanguageKeepEnglishTest extends InstallerTestBase { +class InstallerTranslationMultipleLanguageKeepEnglishTest extends InstallerTranslationMultipleLanguageForeignTest { /** - * Overrides the language code in which to install Drupal. + * Switch to the multilingual testing profile with English kept. * * @var string */ - protected $langcode = 'de'; - - /** - * Switch to the multilingual testing profile - * - * @var string - */ - protected $profile = 'testing_multilingual'; - - /** - * {@inheritdoc} - */ - protected function setUpLanguage() { - // Place custom local translations in the translations directory. - mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE); - file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue German\""); - file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue Spanish\""); - - parent::setUpLanguage(); - $this->translations['Save and continue'] = 'Save and continue German'; - } - - /** - * Tests that English is still present. - */ - public function testKeepEnglish() { - $this->assertTrue((bool) \Drupal::languageManager()->getLanguage('en'), 'English is present.'); - } + protected $profile = 'testing_multilingual_with_english'; } diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageTest.php index 94759e4..a16369b 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationMultipleLanguageTest.php @@ -17,7 +17,7 @@ class InstallerTranslationMultipleLanguageTest extends InstallerTestBase { /** - * Switch to the multilingual testing profile + * Switch to the multilingual testing profile. * * @var string */ @@ -29,47 +29,117 @@ class InstallerTranslationMultipleLanguageTest extends InstallerTestBase { protected function setUpLanguage() { // Place custom local translations in the translations directory. mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE); - file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue German\"\nmsgid\"Anonymous\"\nmsgstr\"Anonymous German\""); - file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue Spanish\"\nmsgid\"Anonymous\"\nmsgstr\"Anonymous Spanish\""); + file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de')); + file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es')); parent::setUpLanguage(); } /** - * Tests that translations for each language were loaded. + * Returns the string for the test .po file. + * + * @param string $langcode + * The language code. + * @return string + * Contents for the test .po file. + */ + protected function getPo($langcode) { + return <<drupalGet('admin/config/regional/language'); $this->assertText('German'); $this->assertText('Spanish'); - // If the installer was English, we expect that configured also. - if ($this->langcode == 'en') { + // If the installer was English or we used a profile that keeps English, we + // expect that configured also. Otherwise English should not be configured + // on the site. + if ($this->langcode == 'en' || $this->profile == 'testing_multilingual_with_english') { $this->assertText('English'); } + else { + $this->assertNoText('English'); + } // Verify the strings from the translation files were imported. - $test_samples = array('Save and continue', 'Anonymous'); - $languages = array( - 'de' => 'German', - 'es' => 'Spanish', - ); + $test_samples = ['Save and continue', 'Anonymous']; + $langcodes = ['de', 'es']; foreach($test_samples as $sample) { - foreach($languages as $langcode => $name) { + foreach($langcodes as $langcode) { $edit = array(); $edit['langcode'] = $langcode; $edit['translation'] = 'translated'; $edit['string'] = $sample; $this->drupalPostForm('admin/config/regional/translate', $edit, t('Filter')); - $this->assertText($sample . ' ' . $name); + $this->assertText($sample . ' ' . $langcode); + } + } + + /** @var \Drupal\language\ConfigurableLanguageManager $language_manager */ + $language_manager = \Drupal::languageManager(); + + // If the site was installed in a foreign language (only tested with German + // in subclasses), then the active configuration should be updated and no + // override should exist in German. Otherwise the German translation should + // end up in overrides the same way as Spanish (which is not used as a site + // installation language). English should be available based on profile + // information and should be possible to add if not yet added, making + // English overrides available. + + $config = \Drupal::config('user.settings'); + $override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings'); + $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings'); + $override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings'); + + if ($this->langcode == 'de') { + // Active configuration should be in German and no German override should + // exist. + $this->assertEqual($config->get('anonymous'), 'Anonymous de'); + $this->assertEqual($config->get('langcode'), 'de'); + $this->assertTrue($override_de->isNew()); + + if ($this->profile == 'testing_multilingual_with_english') { + // English is already added in this profile. Should make the override + // available. + $this->assertEqual($override_en->get('anonymous'), 'Anonymous'); + } + else { + // English is not yet available. + $this->assertTrue($override_en->isNew()); + + // Adding English should make the English override available. + $edit = ['predefined_langcode' => 'en']; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings'); + $this->assertEqual($override_en->get('anonymous'), 'Anonymous'); } } + else { + // Active configuration should be English. + $this->assertEqual($config->get('anonymous'), 'Anonymous'); + $this->assertEqual($config->get('langcode'), 'en'); + // There should not be an English override. + $this->assertTrue($override_en->isNew()); + // German should be an override. + $this->assertEqual($override_de->get('anonymous'), 'Anonymous de'); + } - $config = \Drupal::languageManager()->getLanguageConfigOverride('de', 'user.settings'); - $this->assertEqual($config->get('anonymous'), 'Anonymous German'); - $config = \Drupal::languageManager()->getLanguageConfigOverride('es', 'user.settings'); - $this->assertEqual($config->get('anonymous'), 'Anonymous Spanish'); + // Spanish is always an override (never used as installation language). + $this->assertEqual($override_es->get('anonymous'), 'Anonymous es'); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php index 8d9305c..7f3a66f 100644 --- a/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerTranslationTest.php @@ -11,8 +11,7 @@ use Drupal\user\Entity\User; /** - * Selects German as the installation language and verifies the following page - * is not in English. + * Installs Drupal in German and checks resulting site. * * @group Installer */ @@ -31,14 +30,15 @@ class InstallerTranslationTest extends InstallerTestBase { protected function setUpLanguage() { // Place a custom local translation in the translations directory. mkdir(\Drupal::root() . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE); - file_put_contents(\Drupal::root() . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue German\""); + file_put_contents(\Drupal::root() . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de')); parent::setUpLanguage(); + // After selecting a different language than English, all following screens // should be translated already. $elements = $this->xpath('//input[@type="submit"]/@value'); - $this->assertEqual((string) current($elements), 'Save and continue German'); - $this->translations['Save and continue'] = 'Save and continue German'; + $this->assertEqual((string) current($elements), 'Save and continue de'); + $this->translations['Save and continue'] = 'Save and continue de'; // Check the language direction. $direction = (string) current($this->xpath('/html/@dir')); @@ -46,12 +46,18 @@ protected function setUpLanguage() { } /** - * Verifies that installation succeeded. + * Verifies the expected behaviors of the installation result. */ public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); + // Verify German was configured but not English. + $this->drupalGet('admin/config/regional/language'); + $this->assertText('German'); + $this->assertNoText('English'); + + /** @var \Drupal\user\Entity\User $account */ $account = User::load(0); $this->assertEqual($account->language()->getId(), 'en', 'Anonymous user is English.'); $account = User::load(1); @@ -68,6 +74,58 @@ public function testInstaller() { $this->drupalPostForm('admin/config/development/performance', $edit, t('Save configuration')); $this->drupalGet(''); $this->assertRaw('classy/css/layout.css'); + + // Verify the strings from the translation files were imported. + $test_samples = ['Save and continue', 'Anonymous']; + foreach($test_samples as $sample) { + $edit = array(); + $edit['langcode'] = 'de'; + $edit['translation'] = 'translated'; + $edit['string'] = $sample; + $this->drupalPostForm('admin/config/regional/translate', $edit, t('Filter')); + $this->assertText($sample . ' de'); + } + + /** @var \Drupal\language\ConfigurableLanguageManager $language_manager */ + $language_manager = \Drupal::languageManager(); + + // Installed in German, configuration should be in German. No German or + // English overrides should be present. + $config = \Drupal::config('user.settings'); + $override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings'); + $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings'); + $this->assertEqual($config->get('anonymous'), 'Anonymous de'); + $this->assertEqual($config->get('langcode'), 'de'); + $this->assertTrue($override_de->isNew()); + $this->assertTrue($override_en->isNew()); + + // Assert that adding English makes the English override available. + $edit = ['predefined_langcode' => 'en']; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + $override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings'); + $this->assertFalse($override_en->isNew()); + $this->assertEqual($override_en->get('anonymous'), 'Anonymous'); + } + + /** + * Returns the string for the test .po file. + * + * @param string $langcode + * The language code. + * @return string + * Contents for the test .po file. + */ + protected function getPo($langcode) { + return <<