diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 34a9940..cd2296d 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -729,6 +729,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'),
),
@@ -1830,6 +1836,20 @@ 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.
+ */
+function install_update_configuration_translations(&$install_state) {
+ module_load_include('bulk.inc', 'locale');
+ return locale_config_batch_update_components(array('langcode' => $install_state['parameters']['langcode']));
+}
+
+/**
* Performs final installation steps and displays a 'finished' page.
*
* @param $install_state
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index 2968e7a..2ada0d6 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -88,6 +88,10 @@ public function build(ContainerBuilder $container) {
$container
->register('config.storage.schema', 'Drupal\Core\Config\Schema\SchemaStorage');
+ // Register installer configuration storage
+ $container
+ ->register('config.storage.installer', 'Drupal\Core\Config\InstallStorage');
+
// Register the typed configuration data manager.
$container->register('config.typed', 'Drupal\Core\Config\TypedConfigManager')
->addArgument(new Reference('config.storage'))
diff --git a/core/modules/locale/lib/Drupal/locale/LocaleBundle.php b/core/modules/locale/lib/Drupal/locale/LocaleBundle.php
index 7c2ca6e..61eae0a 100644
--- a/core/modules/locale/lib/Drupal/locale/LocaleBundle.php
+++ b/core/modules/locale/lib/Drupal/locale/LocaleBundle.php
@@ -24,6 +24,11 @@ public function build(ContainerBuilder $container) {
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('config.context'))
->addTag('event_subscriber');
+ // Register the locale configuration data manager.
+ $container->register('locale.config.typed', 'Drupal\locale\LocaleConfigManager')
+ ->addArgument(new Reference('config.storage'))
+ ->addArgument(new Reference('config.storage.schema'))
+ ->addArgument(new Reference('config.storage.installer'));
}
}
diff --git a/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php
new file mode 100644
index 0000000..3c20d16
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php
@@ -0,0 +1,199 @@
+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) {
+ // Note we get only the data that didn't change from default.
+ $default = $this->installStorage->read($name);
+ // Unless the configuration has a explicit language code we assume English.
+ $langcode = isset($default['langcode']) ? $default['langcode'] : 'en';
+ $updated = $this->configStorage->read($name);
+ $data = $this->compareConfigData($default, $updated);
+ $definition = $this->getDefinition($name);
+ $config_typed = $this->create($definition, $data);
+ return new LocaleTypedConfig($name, $langcode, $config_typed, $this->localeStorage);
+ }
+
+ /**
+ * Compare default configuration with updated data.
+ *
+ * @param array $default
+ * Default configuration data.
+ * @param array $updated
+ * Current configuration data.
+ *
+ * @return array
+ * The elements of default configuration that haven't changed.
+ */
+ protected function compareConfigData($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;
+ }
+
+ /**
+ * Save translated configuration data.
+ *
+ * @param string $name
+ * @param string $langcode
+ * @param array $data
+ */
+ public function saveConfigData($name, $langcode, $data) {
+ $locale_name = 'locale.config.' . $langcode . '.' . $name;
+ $this->configStorage->write($locale_name, $data);
+ }
+
+ /**
+ * Save translated configuration data.
+ *
+ * @param string $name
+ * @param string $langcode
+ * @param array $data
+ */
+ public function deleteConfigData($name, $langcode) {
+ $locale_name = 'locale.config.' . $langcode . '.' . $name;
+ $this->configStorage->delete($locale_name);
+ }
+
+ /**
+ * Gets configuration names associated with components.
+ *
+ * @param array $components
+ * Array with string identifiers.
+ *
+ * @return array
+ * Array of configuration object names.
+ */
+ public function getComponentNames($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 for uninstalled components.
+ *
+ * @param array $components
+ * Array with string identifiers.
+ * @param array $langcodes
+ * Array of language codes
+ */
+ public function deleteComponents($components, $langcodes) {
+ $names = $this->getComponentNames($components);
+ if ($names && $langcodes) {
+ foreach ($names as $name) {
+ foreach (array_keys($langcodes) as $langcode) {
+ $this->deleteConfigData($name, $langcode);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get configuration names associated with strings.
+ *
+ * @param array $lids
+ * Array with string identifiers.
+ *
+ * @return array
+ * Array of configuration object names.
+ */
+ public function getStringNames($lids) {
+ $names = array();
+ $locations = $this->localeStorage->getLocations(array('sid' => $lids, 'type' => 'configuration'));
+ foreach ($locations as $location) {
+ $names[$location->name] = $location->name;
+ }
+ return $names;
+ }
+
+ /**
+ * Delete configuration for language.
+ *
+ * @param $langcode
+ * Language code to delete.
+ */
+ public function deleteLanguage($langcode) {
+ $locale_name = 'locale.config.' . $langcode;
+ foreach ($this->configStorage->listAll($locale_name) as $name) {
+ $this->configStorage->delete($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..b4c69ed
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/LocaleTypedConfig.php
@@ -0,0 +1,316 @@
+name = $name;
+ $this->langcode = $langcode;
+ $this->typedConfig = $typedConfig;
+ $this->localeStorage = $localeStorage;
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::getType().
+ */
+ public function getType() {
+ return $this->typedConfig->getType();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::getDefinition().
+ */
+ public function getDefinition() {
+ return $this->typedConfig->getDefinition();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
+ */
+ public function getValue() {
+ return $this->typedConfig->getValue();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::setValue().
+ */
+ public function setValue($value) {
+ $this->typedConfig->setValue($value);
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::getString().
+ */
+ public function getString() {
+ return $this->typedConfig->getString();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::getConstraints().
+ */
+ public function getConstraints() {
+ return $this->typedConfig->getConstraints();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\TypedDataInterface::validate().
+ */
+ public function validate() {
+ return $this->typedConfig->validate();
+ }
+
+
+ /**
+ * Implements \Drupal\Core\TypedData\ContextAwareInterface::getName().
+ */
+ public function getName() {
+ return $this->typedConfig->getName();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\ContextAwareInterface::getRoot().
+ */
+ public function getRoot() {
+ return $this->typedConfig->getRoot();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\ContextAwareInterface::getPropertyPath().
+ */
+ public function getPropertyPath() {
+ return $this->typedConfig->getPropertyPath();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\ContextAwareInterface::getParent().
+ *
+ * @return \Drupal\Core\Entity\Field\FieldInterface
+ */
+ public function getParent() {
+ return $this->typedConfig->getParent();
+ }
+
+ /**
+ * Implements \Drupal\Core\TypedData\ContextAwareInterface::setContext().
+ */
+ public function setContext($name = NULL, ContextAwareInterface $parent = NULL) {
+ $this->typedConfig->setContext($name, $parent);
+ }
+
+ /**
+ * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
+ */
+ public function getTranslationLanguages($include_default = TRUE) {
+ $languages = locale_translatable_language_list();
+ if ($include_default) {
+ $default = $this->language();
+ $languages[$default->langcode] = $default;
+ }
+ else {
+ unset($languages[$this->langcode]);
+ }
+ return $languages;
+ }
+
+ /**
+ * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslation().
+ */
+ public function getTranslation($langcode, $strict = TRUE) {
+ $options = array(
+ 'source' => $this->language()->langcode,
+ 'target' => $langcode,
+ 'strict' => $strict,
+ );
+ $data = $this->getTranslatedData($this->typedConfig, $options);
+ $translation = clone $this->typedConfig;
+ $translation->setValue($data);
+ return $translation;
+ }
+
+ /**
+ * Implements Drupal\Core\TypedData\TranslatableInterface::language().
+ */
+ 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 boolean
+ * TRUE if this translator supports translations for these languages.
+ */
+ protected function canTranslate($from_langcode, $to_langcode) {
+ if ($from_langcode == 'en') {
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Get translated configuration data.
+ *
+ * @param \Traversable $elements
+ * Configuration elements.
+ * @param array $options
+ * Array with options that will depend on the translator used.
+ *
+ * @return array
+ * Configuration data translated to the requested language.
+ */
+ protected function getTranslatedData($elements, $options) {
+ $strict = !empty($options['strict']);
+ $translation = array();
+ foreach ($elements as $key => $element) {
+ $value = NULL;
+ if ($element instanceof Element) {
+ $value = $this->getTranslatedData($element, $options);
+ }
+ elseif ($this->translateElement($element, $options)) {
+ $value = $element->getValue();
+ }
+ elseif (!$strict) {
+ $value = $element->getValue();
+ }
+ if ($value || !$strict) {
+ $translation[$key] = $value;
+ }
+ }
+ return $translation;
+ }
+
+ /**
+ * Translates element if it fits our translation criteria.
+ *
+ * For an element to be translatable by locale module it needs to be of type
+ * 'text'. 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 are dependent on the translator.
+ *
+ * @return bool
+ * Whether the element fits the translation criteria.
+ */
+ protected function translateElement($element, $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->translateString($options['target'], $value, $context)) {
+ $element->setValue($translation);
+ return TRUE;
+ }
+ }
+ }
+ // The element doesn't have a translation, if strict mode we drop it.
+ return empty($options['strict']);
+ }
+
+ /**
+ * Translates string using the localization system.
+ *
+ * So far we only know how to translate strings from English so we check
+ * whether the source data is English.
+ * Unlike regular t() translations, strings will be added to the source
+ * tables only if this is marked as default data.
+ *
+ * @param string $langcode
+ * Language code to translate to.
+ * @param string $source
+ * The source string.
+ * @param string $context
+ * The string context.
+ *
+ * @return string|bool
+ * Translated string if there is a translation, FALSE if not.
+ */
+ protected function translateString($langcode, $source, $context) {
+ if ($source) {
+ // Preload all translations for this configuration name and language.
+ if (!isset($this->translations[$langcode])) {
+ $this->translations[$langcode] = array();
+ foreach ($this->localeStorage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => $this->name)) as $string){
+ $this->translations[$langcode][$string->context][$string->source] = $string;
+ }
+ }
+ if (!isset($this->translations[$langcode][$context][$source])) {
+ if ($translation = $this->localeStorage->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode))) {
+ // The translation was there but the location was missing.
+ // Convert to SourceString because it may not have translation.
+ $string = $this->localeStorage->createString((array) $translation)
+ ->addLocation('configuration', $this->name)
+ ->save();
+ }
+ else {
+ // Add missing source string with the right location.
+ $translation = $this->localeStorage
+ ->createString(array('source' => $source, 'context' => $context))
+ ->addLocation('configuration', $this->name)
+ ->save();
+ }
+ $this->translations[$langcode][$context][$source] = $translation;
+ }
+ $translation = $this->translations[$langcode][$context][$source];
+ return $translation->isTranslation() ? $translation->getString() : FALSE;
+ }
+ return FALSE;
+ }
+
+}
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..bac906f
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigTranslationTest.php
@@ -0,0 +1,154 @@
+ 'Configuration translation',
+ 'description' => 'Tests translation of configuration strings.',
+ 'group' => 'Locale',
+ );
+ }
+
+ 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'));
+
+ $typed_config = config_typed()->get('system.site');
+ $wrapper = new LocaleTypedConfig('system.site', 'en', $typed_config, $this->storage);
+
+ // Get strict translation and check we've got only the site name.
+ $translation = $wrapper->getTranslation($langcode, TRUE);
+ $properties = $translation->get()->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've got all properties.
+ $translation = $wrapper->getTranslation($langcode, FALSE);
+ $properties = $translation->get()->getProperties();
+ $this->assertTrue(count($properties) == 6 && count($wrapper->get('page')) == 3, '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.');
+
+ // This really should be testing entity_load_multiple.
+ $typed_config = config_typed()->get('image.style.medium');
+ $wrapper = new LocaleTypedConfig('image.style.medium', 'en', $typed_config, $this->storage);
+ $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/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 6a452d8..0547aa3 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -6,9 +6,12 @@
*/
use Drupal\Component\Gettext\PoStreamWriter;
+use Drupal\locale\LocaleTypedConfig;
use Drupal\locale\Gettext;
use Drupal\locale\PoDatabaseReader;
use Drupal\Core\Language\Language;
+use Drupal\Core\Config\InstallStorage;
+use Drupal\Core\Config\StorageException;
/**
@@ -123,6 +126,7 @@ function locale_translate_import_form_submit($form, &$form_state) {
'langcode' => $form_state['values']['langcode'],
'overwrite_options' => $form_state['values']['overwrite_options'],
'customized' => $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED,
+ 'refresh_configuration' => TRUE,
);
$file = locale_translate_file_attach_properties($file, $options);
$batch = locale_translate_batch_build(array($file->uri => $file), $options);
@@ -277,8 +281,12 @@ function locale_translate_export_form_submit($form, &$form_state) {
* are customized translations or come from a community source. Use
* LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
* LOCALE_NOT_CUSTOMIZED.
+ * - 'refresh_configuration': Whether or not to refresh configuration strings
+ * after the import. Optional, defaults to FALSE.
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
+ * - 'components': Array of arrays of components to refresh the configuration
+ * indexed by type ('module' or 'theme'). Optional, defaults to none.
*
* @param $force
* (optional) Import all available files, even if they were imported before.
@@ -389,6 +397,7 @@ function locale_translate_batch_build($files, $options) {
'overwrite_options' => array(),
'customized' => LOCALE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
+ 'refresh_configuration' => FALSE,
);
$t = get_t();
if (count($files)) {
@@ -399,6 +408,8 @@ 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($options));
$batch = array(
'operations' => $operations,
@@ -537,12 +548,66 @@ function locale_translate_batch_import_save($context) {
}
/**
+ * Refresh translations after importing strings.
+ *
+ * @param array $options
+ * An array with options that can have the following elements:
+ * - 'refresh_configuration': Whether or not to refresh configuration strings
+ * after the import. Optional, defaults to FALSE.
+ *
+ * @param array $context
+ * Contains a list of strings updated and information about the progress.
+ */
+function locale_translate_batch_refresh($options, &$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) {
+ $context['message'] = t('Updating translations for JavaScript and Configuration strings.');
+ $strings = array_unique($strings);
+ // Clear cache and force refresh of JavaScript translations.
+ _locale_refresh_translations($langcodes, $strings);
+ // Check whether we need to refresh configuration objects.
+ if ($options['refresh_configuration'] && $names = locale_config()->getStringNames($strings)) {
+ $context['sandbox']['refresh']['names'] = $names;
+ $context['sandbox']['refresh']['languages'] = $langcodes;
+ $context['sandbox']['refresh']['count'] = count($names);
+ $context['results']['stats']['config'] = 0;
+ }
+ }
+ if (isset($context['sandbox']['refresh'])) {
+ // We will update configuration on next steps.
+ $context['finished'] = 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;
+ // Not perfect but will give some idea of progress.
+ $context['finished'] = 1 - count($context['sandbox']['refresh']['names']) / $context['sandbox']['refresh']['count'];
+ }
+ 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;
+ $strings = $langcodes = array();
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')));
@@ -565,8 +630,10 @@ function locale_translate_batch_finished($success, $results) {
if ($report['skips'] > 0) {
$skipped_files[] = $filepath;
}
+ $strings = array_merge($strings, $report['strings']);
}
- // Get list of unique language codes updated.
+ // Get list of unique string identifiers and language codes updated.
+ $strings = array_unique($strings);
$langcodes = array_unique(array_values($results['languages']));
}
drupal_set_message(format_plural(count($results['files']),
@@ -587,8 +654,14 @@ function locale_translate_batch_finished($success, $results) {
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);
+ if ($strings) {
+ // Clear cache and force refresh of JavaScript translations.
+ _locale_refresh_translations($langcodes);
+ // Merge feedback about configuration updates too.
+ if (isset($results['stats']['config'])) {
+ locale_config_batch_finished($success, $results);
+ }
+ }
}
}
}
@@ -685,3 +758,131 @@ function locale_translate_delete_translation_files($projects = array(), $langcod
}
return !$fail;
}
+
+/**
+ * Build a locale batch to refresh configuration.
+ *
+ * @param array $options
+ * 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. Optional, defaults to TRUE.
+ * @param array $langcodes
+ * 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($options, $langcodes = array(), $components = array()) {
+ $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+ if ($langcodes && $names = 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 to update.
+ * @param array $langcodes
+ * List of language codes to refresh.
+ * @param array $options
+ * 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. Optional, defaults to TRUE.
+ *
+ * @return array
+ * The batch definition.
+ */
+function locale_config_batch_build($names, $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 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;
+}
+
+/**
+ * Perform configuration translation refresh as a batch step.
+ *
+ * @param array $name
+ * Name of configuration object to update.
+ * @param array $langcodes
+ * (optional) Array of language codes to update. Defaults to all languages.
+ * @param $context
+ * Contains a list of files imported.
+ */
+function locale_config_batch_refresh_name($name, $langcodes, &$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;
+}
+
+/**
+ * Finished callback of system page locale import batch.
+ *
+ * @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, $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);
+ }
+ }
+}
+
+/**
+ * Update 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($names, $langcodes = array()) {
+ $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+ $count = 0;
+ foreach ($names as $name) {
+ $wrapper = locale_config()->get($name);
+ foreach ($langcodes as $langcode) {
+ $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode, TRUE)->getValue() : NULL;
+ if ($translation) {
+ locale_config()->saveConfigData($name, $langcode, $translation);
+ $count++;
+ }
+ else {
+ locale_config()->deleteConfigData($name, $langcode);
+ }
+ }
+ }
+ return $count;
+}
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index fb72927..7d7816d 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -321,6 +321,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.
+ locale_config()->deleteLanguage($language->langcode);
+
// Changing the language settings impacts the interface:
_locale_invalidate_js($language->langcode);
cache('page')->deleteAll();
@@ -475,14 +478,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);
}
/**
@@ -492,14 +497,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);
}
/**
@@ -508,11 +515,15 @@ function locale_themes_disabled($themes) {
* This function will start a batch to import translations for the added
* components.
*
- * @param array $components
- * An array of component (theme and/or module) names to import
- * translations for.
+ * @param $components
+ * An array of arrays of component (theme and/or module) names to import
+ * translations for, indexed by type.
*/
function locale_system_update($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.
@@ -523,11 +534,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);
+ }
+ module_load_include('bulk.inc', 'locale');
+ if ($batch = locale_config_batch_update_components(array(), array(), $components)) {
batch_set($batch);
}
}
@@ -541,37 +556,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');
+ module_load_include('bulk.inc', 'locale');
+ // Delete configuration translations.
+ locale_config()->deleteComponents($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().
*
@@ -774,6 +793,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.
+ module_load_include('bulk.inc', 'locale');
+ if ($batch = locale_config_batch_update_components($options, array($langcode))) {
+ batch_set($batch);
+ }
}
/**
@@ -1053,6 +1078,25 @@ function _locale_refresh_translations($langcodes, $lids = array()) {
}
/**
+ * Refresh 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($langcodes, $lids) {
+ if ($lids && $langcodes && $names = locale_config()->getStringNames($lids)) {
+ module_load_include('bulk.inc', 'locale');
+ 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.
*
@@ -1312,3 +1356,17 @@ function _locale_rebuild_js($langcode = NULL) {
return TRUE;
}
}
+
+/**
+ * Returns the locale config manager service.
+ *
+ * Use the locale config manager service for creating locale-wrapped typed
+ * configuration objects.
+ *
+ * @see Drupal\Core\TypedData\TypedDataManager::create()
+ *
+ * @return Drupal\locale\LocaleConfigManager
+ */
+function locale_config() {
+ return drupal_container()->get('locale.config.typed');
+}
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 4366871..c028c8e 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -452,8 +452,8 @@ function locale_translate_edit_form_submit($form, &$form_state) {
}
if ($updated) {
- // Clear cache and force refresh of JavaScript translations.
- _locale_refresh_translations(array($langcode), $updated);
+ // Clear cache and refresh configuration and JavaScript translations.
+ _locale_refresh_configuration(array($langcode), $updated);
}
}
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 ac9bc78..8a7fef6 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: