diff --git a/core/core.services.yml b/core/core.services.yml index 9fb9ebd..8b2f0dd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -86,6 +86,8 @@ services: arguments: ['@database', config_snapshot] config.storage.schema: class: Drupal\Core\Config\Schema\SchemaStorage + config.storage.installer: + class: Drupal\Core\Config\InstallStorage config.typed: class: Drupal\Core\Config\TypedConfigManager arguments: ['@config.storage', '@config.storage.schema'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 47982dd..6617366 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -760,6 +760,12 @@ function install_tasks($install_state) { 'type' => 'batch', 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, ), + 'install_update_configuration_translations' => array( + 'display_name' => st('Translate configuration'), + 'display' => $needs_translations, + 'type' => 'batch', + 'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP, + ), 'install_finished' => array( 'display_name' => st('Finished'), ), @@ -1855,6 +1861,22 @@ function install_import_translations_remaining(&$install_state) { } /** + * Creates configuration translations. + * + * @param array $install_state + * An array of information about the current installation state. + * + * @return array + * The batch definition, if there are configuration objects to update. + * + * @see install_tasks() + */ +function install_update_configuration_translations(&$install_state) { + Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); + return locale_config_batch_update_components(array(), array($install_state['parameters']['langcode'])); +} + +/** * Performs final installation steps and displays a 'finished' page. * * @param $install_state diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigLocaleOverrideWebTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigLocaleOverrideWebTest.php index 9f9579d..204e274 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigLocaleOverrideWebTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigLocaleOverrideWebTest.php @@ -14,7 +14,7 @@ */ class ConfigLocaleOverrideWebTest extends WebTestBase { - public static $modules = array('locale', 'language', 'system', 'config_test'); + public static $modules = array('locale', 'language', 'system'); public static function getInfo() { return array( @@ -35,26 +35,35 @@ function testSiteNameTranslation() { $adminUser = $this->drupalCreateUser(array('administer site configuration', 'administer languages')); $this->drupalLogin($adminUser); - // Add French and make it the site default language. - $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'fr'), t('Add language')); + // Add a custom lanugage. + $langcode = 'xx'; + $name = $this->randomName(16); + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Save an override for the XX language. + config('locale.config.xx.system.site')->set('name', 'XX site name')->save(); $this->drupalLogout(); // The home page in English should not have the override. $this->drupalGet(''); - $this->assertNoText('French site name'); + $this->assertNoText('XX site name'); // During path resolution the system.site configuration object is used to // determine the front page. This occurs before language negotiation causing // the configuration factory to cache an object without the correct - // overrides. The config_test module includes a - // locale.config.fr.system.site.yml which overrides the site name to 'French - // site name' to test that the configuration factory is re-initialised - // language negotiation. Ensure that it applies when we access the French - // front page. + // overrides. We are testing that the configuration factory is + // re-initialised after language negotiation. Ensure that it applies when + // we access the XX front page. // @see \Drupal\Core\PathProcessor::processInbound() - $this->drupalGet('fr'); - $this->assertText('French site name'); + $this->drupalGet('xx'); + $this->assertText('XX site name'); } } diff --git a/core/modules/config/tests/config_test/config/locale.config.fr.system.site.yml b/core/modules/config/tests/config_test/config/locale.config.fr.system.site.yml deleted file mode 100644 index 0e4081b..0000000 --- a/core/modules/config/tests/config_test/config/locale.config.fr.system.site.yml +++ /dev/null @@ -1 +0,0 @@ -name: 'French site name' diff --git a/core/modules/locale/lib/Drupal/locale/Locale.php b/core/modules/locale/lib/Drupal/locale/Locale.php new file mode 100644 index 0000000..f4f41c1 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Locale.php @@ -0,0 +1,30 @@ +installStorage = $installStorage; + $this->localeStorage = $localeStorage ?: locale_storage(); + } + + /** + * Gets locale wrapper with typed configuration data. + * + * @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->installStorage->read($name); + $updated = $this->configStorage->read($name); + // We get only the data that didn't change from default. + $data = $this->compareConfigData($default, $updated); + $definition = $this->getDefinition($name); + // Unless the configuration has a explicit language code we assume English. + $langcode = isset($default['langcode']) ? $default['langcode'] : 'en'; + $wrapper = new LocaleTypedConfig($definition, $name, $langcode, $this); + $wrapper->setValue($data); + return $wrapper; + } + + /** + * Compares default configuration with updated data. + * + * @param array $default + * Default configuration data. + * @param array|false $updated + * Current configuration data, or FALSE if no configuration data existed. + * + * @return array + * The elements of default configuration that haven't changed. + */ + protected function compareConfigData(array $default, $updated) { + // Speed up comparison, specially for install operations. + if ($default === $updated) { + return $default; + } + $result = array(); + foreach ($default as $key => $value) { + if (isset($updated[$key])) { + if (is_array($value)) { + $result[$key] = $this->compareConfigData($value, $updated[$key]); + } + elseif ($value === $updated[$key]) { + $result[$key] = $value; + } + } + } + return $result; + } + + /** + * Saves translated configuration data. + * + * @param string $name + * Configuration object name. + * @param string $langcode + * Language code. + * @param array $data + * Configuration data to be saved, that will be only the translated values. + */ + public function saveTranslationData($name, $langcode, array $data) { + $locale_name = self::localeConfigName($langcode, $name); + $this->configStorage->write($locale_name, $data); + } + + /** + * Deletes translated configuration data. + * + * @param string $name + * Configuration object name. + * @param string $langcode + * Language code. + */ + public function deleteTranslationData($name, $langcode) { + $locale_name = self::localeConfigName($langcode, $name); + $this->configStorage->delete($locale_name); + } + + /** + * Gets configuration names associated with components. + * + * @param array $components + * (optional) Array of component lists indexed by type. If not present or it + * is an empty array, it will update all components. + * + * @return array + * Array of configuration object names. + */ + public function getComponentNames(array $components) { + $components = array_filter($components); + if ($components) { + $names = array(); + foreach ($components as $type => $list) { + // InstallStorage::getComponentNames returns a list of folders keyed by + // config name. + $names = array_merge($names, array_keys($this->installStorage->getComponentNames($type, $list))); + } + return $names; + } + else { + return $this->installStorage->listAll(); + } + } + + /** + * 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) { + $names = $this->getComponentNames($components); + if ($names && $langcodes) { + foreach ($names as $name) { + foreach ($langcodes as $langcode) { + $this->deleteTranslationData($name, $langcode); + } + } + } + } + + /** + * Gets configuration names associated with strings. + * + * @param array $lids + * Array with string identifiers. + * + * @return array + * Array of configuration object names. + */ + public function getStringNames(array $lids) { + $names = array(); + $locations = $this->localeStorage->getLocations(array('sid' => $lids, 'type' => 'configuration')); + foreach ($locations as $location) { + $names[$location->name] = $location->name; + } + return $names; + } + + /** + * Deletes configuration for language. + * + * @param string $langcode + * Language code to delete. + */ + public function deleteLanguageTranslations($langcode) { + $locale_name = self::localeConfigName($langcode); + foreach ($this->configStorage->listAll($locale_name) as $name) { + $this->configStorage->delete($name); + } + } + + /** + * Translates string using the localization system. + * + * So far we only know how to translate strings from English so the source + * string should be in English. + * Unlike regular t() translations, strings will be added to the source + * tables only if this is marked as default data. + * + * @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 string|false + * Translated string if there is a translation, FALSE if not. + */ + public function translateString($name, $langcode, $source, $context) { + if ($source) { + // If translations for a language have not been loaded yet. + if (!isset($this->translations[$name][$langcode])) { + // Preload all translations for this configuration name and language. + $this->translations[$name][$langcode] = array(); + foreach ($this->localeStorage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => $name)) as $string){ + $this->translations[$name][$langcode][$string->context][$string->source] = $string; + } + } + if (!isset($this->translations[$name][$langcode][$context][$source])) { + // There is no translation of the source string in this config location + // to this language for this context. + if ($translation = $this->localeStorage->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode))) { + // Look for a translation of the string. It might have one, but not + // be saved in this configuration location yet. + // If the string has a translation for this context to this language, + // save it in the configuration location so it can be looked up faster + // next time. + $string = $this->localeStorage->createString((array) $translation) + ->addLocation('configuration', $name) + ->save(); + } + else { + // No translation was found. Add the source to the configuration + // location so it can be translated, and the string is faster to look + // for next time. + $translation = $this->localeStorage + ->createString(array('source' => $source, 'context' => $context)) + ->addLocation('configuration', $name) + ->save(); + } + + // Add an entry, either the translation found, or a blank string object + // to track the source string, to this configuration location, language, + // and context. + $this->translations[$name][$langcode][$context][$source] = $translation; + } + + // Return the string only when the string object had a translation. + if ($this->translations[$name][$langcode][$context][$source]->isTranslation()) { + return $this->translations[$name][$langcode][$context][$source]->getString(); + } + } + return FALSE; + } + + /** + * Provides configuration data location for given langcode and name. + * + * @param string $langcode + * The language code. + * @param string|NULL $name + * Name of the original configuration. Set to NULL to get the name prefix + * for all $langcode overrides. +loca * + * @return string + */ + public static function localeConfigName($langcode, $name = NULL) { + return rtrim('locale.config.' . $langcode . '.' . $name, '.'); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/LocaleTypedConfig.php b/core/modules/locale/lib/Drupal/locale/LocaleTypedConfig.php new file mode 100644 index 0000000..cef4fb1 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/LocaleTypedConfig.php @@ -0,0 +1,207 @@ +langcode = $langcode; + $this->localeConfig = $localeConfig; + } + + /** + * Gets wrapped typed config object. + */ + public function getTypedConfig() { + return $this->localeConfig->create($this->definition, $this->value); + } + + /** + * {@inheritdoc} + */ + public function getTranslationLanguages($include_default = TRUE) { + // Configuration may be in English (when from shipped config) even on a + // site where English is not a configured language. + $languages = locale_translatable_language_list(); + if ($include_default) { + $default = $this->language(); + $languages[$default->langcode] = $default; + } + else { + unset($languages[$this->langcode]); + } + return $languages; + } + + /** + * {@inheritdoc} + */ + public function getTranslation($langcode, $strict = TRUE) { + $options = array( + 'source' => $this->langcode, + 'target' => $langcode, + 'strict' => $strict, + ); + $data = $this->getElementTranslation($this->getTypedConfig(), $options); + return $this->localeConfig->create($this->definition, $data); + } + + /** + * {@inheritdoc} + */ + public function language() { + return new Language(array('langcode' => $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 mixed $element + * Typed configuration element, either \Drupal\Core\Config\Schema\Element or + * \Drupal\Core\Config\Schema\ArrayElement. + * @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($element, array $options) { + $translation = array(); + if ($element instanceof ArrayElement) { + $translation = $this->getArrayTranslation($element, $options); + } + elseif ($this->translateElement($element, $options)) { + $translation = $element->getValue(); + } + return $translation; + } + + /** + * Gets translated configuration data for an element of type ArrayElement. + * + * @param \Drupal\Core\Config\Schema\ArrayElement $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(ArrayElement $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. + * - 'locale 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. + * - 'strict', True to return only elements that actually have translation. + * + * @return bool + * Whether the element fits the translation criteria. + */ + protected function translateElement(\Drupal\Core\TypedData\TypedDataInterface $element, array $options) { + if ($this->canTranslate($options['source'], $options['target'])) { + $definition = $element->getDefinition(); + $value = $element->getValue(); + if ($value && !empty($definition['translatable'])) { + $context = isset($definition['locale context']) ? $definition['locale context'] : ''; + if ($translation = $this->localeConfig->translateString($this->name, $options['target'], $value, $context)) { + $element->setValue($translation); + return TRUE; + } + } + } + // The element does not have a translation. If strict mode we drop it. + return empty($options['strict']); + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php index 618b419..126ee04 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -102,6 +102,7 @@ function setReport($report = array()) { 'updates' => 0, 'deletes' => 0, 'skips' => 0, + 'strings' => array(), ); $this->_report = $report; } @@ -259,6 +260,7 @@ private function importString(PoItem $item) { $string->save(); $this->_report['updates']++; } + $this->_report['strings'][] = $string->getId(); return $string->lid; } else { @@ -273,6 +275,7 @@ private function importString(PoItem $item) { ))->save(); $this->_report['additions']++; + $this->_report['strings'][] = $string->getId(); return $string->lid; } } @@ -280,6 +283,7 @@ private function importString(PoItem $item) { // Empty translation, remove existing if instructed. $string->delete(); $this->_report['deletes']++; + $this->_report['strings'][] = $string->lid; return $string->lid; } } diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigTranslationTest.php new file mode 100644 index 0000000..f4d34ac --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigTranslationTest.php @@ -0,0 +1,158 @@ + 'Configuration translation', + 'description' => 'Tests translation of configuration strings.', + 'group' => 'Locale', + ); + } + + public function setUp() { + parent::setUp(); + // Add a default locale storage for all these tests. + $this->storage = locale_storage(); + } + + /** + * Tests basic configuration translation. + */ + function testConfigTranslation() { + // Add custom language. + $langcode = 'xx'; + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface', 'administer modules')); + $this->drupalLogin($admin_user); + $name = $this->randomName(16); + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + $language = new Language(array('langcode' => $langcode)); + // Set path prefix. + $edit = array( "prefix[$langcode]" => $langcode ); + $this->drupalPost('admin/config/regional/language/detection/url', $edit, t('Save configuration')); + + // Check site name string exists and create translation for it. + $string = $this->storage->findString(array('source' => 'Drupal', 'context' => '', 'type' => 'configuration')); + $this->assertTrue($string, 'Configuration strings have been created upon installation.'); + + // Translate using the UI so configuration is refreshed. + $site_name = $this->randomName(20); + $search = array( + 'string' => $string->source, + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $textareas = $this->xpath('//textarea'); + $textarea = current($textareas); + $lid = (string) $textarea[0]['name']; + $edit = array( + $lid => $site_name, + ); + $this->drupalPost('admin/config/regional/translate/translate', $edit, t('Save translations')); + + $wrapper = $this->container->get('locale.config.typed')->get('system.site'); + + // Get strict translation and check we've got only the site name. + $translation = $wrapper->getTranslation($langcode, TRUE); + $properties = $translation->getProperties(); + $this->assertEqual(count($properties), 1, 'Got the right number of properties with strict translation'); + $this->assertEqual($properties['name']->getValue(), $site_name, 'Got the right translation for site name with strict translation'); + + // Get non strict translation and check we got all unmodified properties. + // Although system.site contains 7 properties, the test system modified + // the site email address, so we get back 4 properties. + $translation = $wrapper->getTranslation($langcode, FALSE); + $properties = $translation->getProperties(); + $this->assertTrue(count($properties) == 4 && count($translation->get('page')) == 1, 'Got the right number of properties with non strict translation'); + $this->assertEqual($properties['name']->getValue(), $site_name, 'Got the right translation for site name with non strict translation'); + + // Check the translated site name is displayed. + $this->drupalGet($langcode); + $this->assertText($site_name, 'The translated site name is displayed after translations refreshed.'); + + // Assert strings from image module config are not available. + $string = $this->storage->findString(array('source' => 'Medium (220x220)', 'context' => '', 'type' => 'configuration')); + $this->assertFalse($string, 'Configuration strings have been created upon installation.'); + + // Enable the image module. + $this->drupalPost('admin/modules', array('modules[Core][image][enable]' => "1"), t('Save configuration')); + $this->resetAll(); + + $string = $this->storage->findString(array('source' => 'Medium (220x220)', 'context' => '', 'type' => 'configuration')); + $this->assertTrue($string, 'Configuration strings have been created upon installation.'); + $locations = $string->getLocations(); + $this->assertTrue(isset($locations['configuration']) && isset($locations['configuration']['image.style.medium']), 'Configuration string has been created with the right location'); + + // Check the string is unique and has no translation yet. + $translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium')); + $translation = reset($translations); + $this->assertTrue(count($translations) == 1 && $translation->source == $string->source && empty($translation->translation), 'Got only one string for image configuration and has no translation.'); + + // Translate using the UI so configuration is refreshed. + $image_style_label = $this->randomName(20); + $search = array( + 'string' => $string->source, + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $textarea = current($this->xpath('//textarea')); + $lid = (string) $textarea[0]['name']; + $edit = array( + $lid => $image_style_label, + ); + $this->drupalPost('admin/config/regional/translate/translate', $edit, t('Save translations')); + + // Check the right single translation has been created. + $translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium')); + $translation = reset($translations); + $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, TRUE); + $property = $translation->get('label'); + $this->assertEqual($property->getValue(), $image_style_label, 'Got the right translation for image style name with strict translation'); + + // Quick test to ensure translation file exists. + $this->assertEqual(config('locale.config.xx.image.style.medium')->get('label'), $image_style_label); + + // Disable and uninstall the module. + $this->drupalPost('admin/modules', array('modules[Core][image][enable]' => FALSE), t('Save configuration')); + $this->drupalPost('admin/modules/uninstall', array('uninstall[image]' => "image"), t('Uninstall')); + $this->drupalPost(NULL, array(), t('Uninstall')); + + // Ensure that the translated configuration has been removed. + $this->assertFalse(config('locale.config.xx.image.style.medium')->get('label'), 'Translated configuration for image module removed.'); + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php index 06269ab..3ae0ae4 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php @@ -238,6 +238,71 @@ function testEmptyMsgstr() { } /** + * Tests .po file import with configuration translation. + */ + function testConfigPoFile() { + // Values for translations to assert. Config key, original string, + // translation and config property name. + $config_strings = array( + 'system.maintenance' => array( + '@site is currently under maintenance. We should be back shortly. Thank you for your patience.', + '@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.', + 'message', + ), + 'user.role.anonymous' => array( + 'Anonymous user', + 'Névtelen felhasználó', + 'label', + ), + ); + + // Add custom language for testing. + $langcode = 'xx'; + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $this->randomName(16), + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Check for the source strings we are going to translate. Adding the + // custom language should have made the process to export configuration + // strings to interface translation executed. + $locale_storage = locale_storage(); + foreach ($config_strings as $config_string) { + $string = $locale_storage->findString(array('source' => $config_string[0], 'context' => '', 'type' => 'configuration')); + $this->assertTrue($string, 'Configuration strings have been created upon installation.'); + } + + // Import a .po file to translate. + $this->importPoFile($this->getPoFileWithConfig(), array( + 'langcode' => $langcode, + )); + + // Translations got recorded in the interface translation system. + foreach ($config_strings as $config_string) { + $search = array( + 'string' => $config_string[0], + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText($config_string[1], format_string('Translation of @string found.', array('@string' => $config_string[0]))); + } + + $locale_config = $this->container->get('locale.config.typed'); + // Translations got recorded in the config system. + foreach ($config_strings as $config_key => $config_string) { + $wrapper = $locale_config->get($config_key); + $translation = $wrapper->getTranslation($langcode, TRUE); + $properties = $translation->getProperties(); + $this->assertEqual(count($properties), 1, 'Got the right number of properties with strict translation'); + $this->assertEqual($properties[$config_string[2]]->getValue(), $config_string[1]); + } + } + + /** * Helper function: import a standalone .po file in a given language. * * @param $contents @@ -449,6 +514,7 @@ function getPoFileWithEmptyMsgstr() { EOF; } + /** * Helper function that returns a .po file with an empty last item. */ @@ -471,4 +537,26 @@ function getPoFileWithMsgstr() { EOF; } + /** + * Helper function that returns a .po file with configuration translations. + */ + function getPoFileWithConfig() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 8\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "@site is currently under maintenance. We should be back shortly. Thank you for your patience." +msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet." + +msgid "Anonymous user" +msgstr "Névtelen felhasználó" + +EOF; + } + } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 6a452d8..eb32027 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -84,7 +84,7 @@ function locale_translate_import_form($form, &$form_state) { '#type' => 'checkbox', '#states' => array( 'checked' => array( - ':input[name="customized"]' => array('checked' => TRUE), + ':input[name="customized"]' => array('checked' => TRUE), ), ), ); @@ -181,7 +181,7 @@ function locale_translate_export_form($form, &$form_state) { '#tree' => TRUE, '#states' => array( 'invisible' => array( - ':input[name="langcode"]' => array('value' => LANGUAGE_SYSTEM), + ':input[name="langcode"]' => array('value' => LANGUAGE_SYSTEM), ), ), ); @@ -400,6 +400,9 @@ function locale_translate_batch_build($files, $options) { // Save the translation status of all files. $operations[] = array('locale_translate_batch_import_save', array()); + // Add a final step to refresh JavaScript and configuration strings. + $operations[] = array('locale_translate_batch_refresh', array()); + $batch = array( 'operations' => $operations, 'title' => $t('Importing interface translations'), @@ -537,20 +540,74 @@ function locale_translate_batch_import_save($context) { } /** + * Refreshs translations after importing strings. + * + * @param array $context + * Contains a list of strings updated and information about the progress. + */ +function locale_translate_batch_refresh(array &$context) { + if (!isset($context['sandbox']['refresh'])) { + $strings = $langcodes = array(); + if (isset($context['results']['stats'])) { + // Get list of unique string identifiers and language codes updated. + $langcodes = array_unique(array_values($context['results']['languages'])); + foreach ($context['results']['stats'] as $filepath => $report) { + $strings = array_merge($strings, $report['strings']); + } + } + if ($strings) { + // Initialize multi-step string refresh. + $context['message'] = t('Updating translations for JavaScript and configuration strings.'); + $context['sandbox']['refresh']['strings'] = array_unique($strings); + $context['sandbox']['refresh']['languages'] = $langcodes; + $context['sandbox']['refresh']['names'] = array(); + $context['results']['stats']['config'] = 0; + $context['sandbox']['refresh']['count'] = count($strings); + + // We will update strings on later steps. + $context['finished'] = 1 - 1 / $context['sandbox']['refresh']['count']; + } + else { + $context['finished'] = 1; + } + } + 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']); + $context['results']['stats']['config'] += $count; + } + elseif (!empty($context['sandbox']['refresh']['strings'])) { + // Not perfect but will give some indication of progress. + $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count']; + // Pending strings, refresh 100 at a time, get next pack. + $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100); + array_splice($context['sandbox']['refresh']['strings'], 0, count($next)); + // Clear cache and force refresh of JavaScript translations. + _locale_refresh_translations($context['sandbox']['refresh']['languages'], $next); + // Check whether we need to refresh configuration objects. + if ($names = \Drupal\locale\Locale::config()->getStringNames($next)) { + $context['sandbox']['refresh']['names'] = $names; + } + } + else { + $context['finished'] = 1; + } +} + +/** * Finished callback of system page locale import batch. */ function locale_translate_batch_finished($success, $results) { if ($success) { - $additions = $updates = $deletes = $skips = 0; - $langcodes = array(); + $additions = $updates = $deletes = $skips = $config = 0; if (isset($results['failed_files'])) { - if (module_exists('dblog')) { - $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog'))); - } - else { - $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.'); - } - drupal_set_message($message, 'error'); + if (Drupal::moduleHandler()->moduleExists('dblog')) { + $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog'))); + } + else { + $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.'); + } + drupal_set_message($message, 'error'); } if (isset($results['files'])) { $skipped_files = array(); @@ -566,8 +623,6 @@ function locale_translate_batch_finished($success, $results) { $skipped_files[] = $filepath; } } - // Get list of unique language codes updated. - $langcodes = array_unique(array_values($results['languages'])); } drupal_set_message(format_plural(count($results['files']), 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', @@ -586,11 +641,12 @@ function locale_translate_batch_finished($success, $results) { drupal_set_message($message, 'warning'); watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); } - - // Clear cache and force refresh of JavaScript translations. - _locale_refresh_translations($langcodes); } } + // Add messages for configuration too. + if (isset($results['stats']['config'])) { + locale_config_batch_finished($success, $results); + } } /** @@ -611,7 +667,7 @@ function locale_translate_file_create($filepath) { } /** - * Generate file properties from filename and options. + * Generates file properties from filename and options. * * An attempt is made to determine the translation language, project name and * project version from the file name. Supported file name patterns are: @@ -685,3 +741,138 @@ function locale_translate_delete_translation_files($projects = array(), $langcod } return !$fail; } + +/** + * Builds a locale batch to refresh configuration. + * + * @param array $options + * An array with options that can have the following elements: + * - 'finish_feedback': (optional) Whether or not to give feedback to the user + * when the batch is finished. Defaults to TRUE. + * @param array $langcodes + * (optional) Array of language codes. Defaults to all translatable languages. + * @param array $components + * (optional) Array of component lists indexed by type. If not present or it + * is an empty array, it will update all components. + * + * @return array + * The batch definition. + */ +function locale_config_batch_update_components(array $options, $langcodes = array(), $components = array()) { + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + if ($langcodes && $names = \Drupal\locale\Locale::config()->getComponentNames($components)) { + return locale_config_batch_build($names, $langcodes, $options); + } +} + +/** + * Creates a locale batch to refresh specific configuration. + * + * @param array $names + * List of configuration object names (which are strings) to update. + * @param array $langcodes + * List of language codes to refresh. + * @param array $options + * (optional) An array with options that can have the following elements: + * - 'finish_feedback': Whether or not to give feedback to the user when the + * batch is finished. Defaults to TRUE. + * + * @return array + * The batch definition. + * + * @see locale_config_batch_refresh_name() + */ +function locale_config_batch_build(array $names, array $langcodes, $options = array()) { + $options += array('finish_feedback' => TRUE); + $t = get_t(); + foreach ($names as $name) { + $operations[] = array('locale_config_batch_refresh_name', array($name, $langcodes)); + } + $batch = array( + 'operations' => $operations, + 'title' => $t('Updating configuration translations'), + 'init_message' => $t('Starting configuration update'), + 'error_message' => $t('Error updating configuration translations'), + 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc', + ); + if (!empty($options['finish_feedback'])) { + $batch['completed'] = 'locale_config_batch_finished'; + } + return $batch; +} + +/** + * Performs configuration translation refresh as a batch step. + * + * @param string $name + * Name of configuration object to update. + * @param array $langcodes + * (optional) Array of language codes to update. Defaults to all languages. + * @param array $context + * Contains a list of files imported. + * + * @see locale_config_batch_build() + */ +function locale_config_batch_refresh_name($name, array $langcodes, array &$context) { + if (!isset($context['result']['stats']['config'])) { + $context['result']['stats']['config'] = 0; + } + $context['result']['stats']['config'] += locale_config_update_multiple(array($name), $langcodes); + $context['result']['names'][] = $name; + $context['result']['langcodes'] = $langcodes; + $context['finished'] = 1; +} + +/** + * Finishes callback of system page locale import batch. + * + * @see locale_config_batch_build() + * + * @param bool $success + * Information about the success of the batch import. + * @param array $results + * Information about the results of the batch import. + */ +function locale_config_batch_finished($success, array $results) { + if ($success) { + $configuration = isset($results['stats']['config']) ? $results['stats']['config'] : 0; + if ($configuration) { + drupal_set_message(t('The configuration was successfully updated. There are %number configuration objects updated.', array('%number' => $configuration))); + watchdog('locale', 'The configuration was successfully updated. %number configuration objects updated.', array('%number' => $configuration)); + } + else { + drupal_set_message(t('No configuration objects have been updated.')); + watchdog('locale', 'No configuration objects have been updated.', array(), WATCHDOG_WARNING); + } + } +} + +/** + * 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, $langcodes = array()) { + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $count = 0; + foreach ($names as $name) { + $wrapper = \Drupal\locale\Locale::config()->get($name); + foreach ($langcodes as $langcode) { + $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode, TRUE)->getValue() : NULL; + if ($translation) { + \Drupal\locale\Locale::config()->saveTranslationData($name, $langcode, $translation); + $count++; + } + else { + \Drupal\locale\Locale::config()->deleteTranslationData($name, $langcode); + } + } + } + return $count; +} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 880e335..c2da3b5 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -319,6 +319,9 @@ function locale_language_delete($language) { module_load_include('inc', 'locale', 'locale.bulk'); locale_translate_delete_translation_files(array(), array($language->langcode)); + // Remove translated configuration objects. + \Drupal\locale\Locale::config()->deleteLanguageTranslations($language->langcode); + // Changing the language settings impacts the interface: _locale_invalidate_js($language->langcode); cache('page')->deleteAll(); @@ -473,14 +476,16 @@ function locale_get_plural($count, $langcode = NULL) { * Implements hook_modules_installed(). */ function locale_modules_installed($modules) { - locale_system_update($modules); + $components['module'] = $modules; + locale_system_update($components); } /** * Implements hook_modules_uninstalled(). */ function locale_modules_uninstalled($modules) { - locale_system_remove($modules); + $components['module'] = $modules; + locale_system_remove($components); } /** @@ -490,14 +495,16 @@ function locale_modules_uninstalled($modules) { * initial installation. The theme system is missing an installation hook. */ function locale_themes_enabled($themes) { - locale_system_update($themes); + $components['theme'] = $themes; + locale_system_update($components); } /** * Implements hook_themes_disabled(). */ function locale_themes_disabled($themes) { - locale_system_remove($themes); + $components['theme'] = $themes; + locale_system_remove($components); } /** @@ -507,10 +514,14 @@ function locale_themes_disabled($themes) { * components. * * @param array $components - * An array of component (theme and/or module) names to import - * translations for. + * An array of arrays of component (theme and/or module) names to import + * translations for, indexed by type. */ -function locale_system_update($components) { +function locale_system_update(array $components) { + + $components += array('module' => array(), 'theme' => array()); + $list = array_merge($components['module'], $components['theme']); + // Skip running the translation imports if in the installer, // because it would break out of the installer flow. We have // built-in support for translation imports in the installer. @@ -521,11 +532,15 @@ function locale_system_update($components) { // Only when new projects are added the update batch will be triggered. Not // each enabled module will introduce a new project. E.g. sub modules. $projects = array_keys(locale_translation_build_projects()); - if ($components = array_intersect($components, $projects)) { + if ($list = array_intersect($list, $projects)) { module_load_include('fetch.inc', 'locale'); // Get translation status of the projects, download and update translations. $options = _locale_translation_default_update_options(); - $batch = locale_translation_batch_update_build($components, array(), $options); + $batch = locale_translation_batch_update_build($list, array(), $options); + batch_set($batch); + } + Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); + if ($batch = locale_config_batch_update_components(array(), array(), $components)) { batch_set($batch); } } @@ -539,37 +554,41 @@ function locale_system_update($components) { * modules and we have no record of which string is used by which module. * * @param array $components - * An array of component (theme and/or module) names to remove - * translation history. + * An array of arrays of component (theme and/or module) names to import + * translations for, indexed by type. */ function locale_system_remove($components) { - if (locale_translatable_language_list()) { + $components += array('module' => array(), 'theme' => array()); + $list = array_merge($components['module'], $components['theme']); + 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 modules. $projects = array_keys(locale_translation_get_projects()); - if ($components = array_intersect($components, $projects)) { - locale_translation_file_history_delete($components); + if ($list = array_intersect($list, $projects)) { + locale_translation_file_history_delete($list); // Remove translation files. - module_load_include('inc', 'locale', 'locale.bulk'); - locale_translate_delete_translation_files($components, array()); + locale_translate_delete_translation_files($list, array()); // Remove translatable projects. // Followup issue http://drupal.org/node/1842362 to replace the // {locale_project} table. Then change this to a function call. db_delete('locale_project') - ->condition('name', $components) + ->condition('name', $list) ->execute(); // Clear the translation status. - locale_translation_status_delete_projects($components); + locale_translation_status_delete_projects($list); } + } } - /** * Implements hook_js_alter(). * @@ -772,6 +791,12 @@ function locale_form_language_admin_add_form_alter_submit($form, $form_state) { $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. + Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); + if ($batch = locale_config_batch_update_components($options, array($langcode))) { + batch_set($batch); + } } /** @@ -1051,6 +1076,25 @@ 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); + } +} + +/** * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and * Drupal.formatPlural() and inserts them into the database. * @@ -1294,8 +1338,8 @@ function _locale_rebuild_js($langcode = NULL) { return TRUE; case 'rebuilt': watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $locale_javascripts[$language->langcode]), WATCHDOG_WARNING); - // Proceed to the 'created' case as the JavaScript translation file has - // been created again. + // Proceed to the 'created' case as the JavaScript translation file has + // been created again. case 'created': watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => $language->name)); return TRUE; diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index fc68a51..a109f5f 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -452,8 +452,9 @@ function locale_translate_edit_form_submit($form, &$form_state) { } if ($updated) { - // Clear cache and force refresh of JavaScript translations. + // Clear cache and refresh configuration and JavaScript translations. _locale_refresh_translations(array($langcode), $updated); + _locale_refresh_configuration(array($langcode), $updated); } } diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index 012a7d9..15763a4 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -4,3 +4,6 @@ services: tags: - { name: event_subscriber } arguments: ['@language_manager', '@config.context'] + locale.config.typed: + class: Drupal\locale\LocaleConfigManager + arguments: ['@config.storage', '@config.storage.schema', '@config.storage.installer'] diff --git a/core/modules/system/config/schema/system.data_types.schema.yml b/core/modules/system/config/schema/system.data_types.schema.yml index 8b7c899..d3b5037 100644 --- a/core/modules/system/config/schema/system.data_types.schema.yml +++ b/core/modules/system/config/schema/system.data_types.schema.yml @@ -37,6 +37,7 @@ default: label: type: string label: 'Label' + translatable: true # Internal Drupal path path: @@ -47,6 +48,7 @@ path: text: type: string label: 'Text' + translatable: true # Complex extended data types: