diff --git a/core/includes/config.inc b/core/includes/config.inc index 5e07c54..18a2078 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -3,6 +3,8 @@ use Drupal\Core\Config\Config; use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\NullStorage; +use Drupal\Core\Config\Metadata\ConfigWrapper; +use Drupal\Core\Config\Metadata\MetadataLookup; use Drupal\Core\Config\StorageInterface; /** @@ -104,6 +106,36 @@ function config_sync_get_changes(StorageInterface $source_storage, StorageInterf } /** + * Retrieves metadata for a configuration object or key. + * + * @param string $name + * The name or key of the configuration object. + * + * @return Drupal\Core\Config\ConfigMetadata + * A metadata array. + */ +function config_metadata($name) { + $metadata = &drupal_static(__FUNCTION__); + if (!$metadata) { + $metadata = new MetadataLookup(); + } + return $metadata[$name]; +} + +/** + * Retrieves a configuration wrapper object to access data as typed properties. + * + * @param string $name + * The name of the configuration object to retrieve. + * + * @return Drupal\Core\Config\Metadata\Wrapper + * A configuration wrapper object. + */ +function config_wrapper($name) { + return new ConfigWrapper($name, config($name)->get()); +} + +/** * Writes an array of config file changes from a source storage to a target storage. * * @param array $config_changes diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 3968682..0bbc549 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -693,6 +693,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'), ), @@ -1670,6 +1676,20 @@ function install_import_translations_remaining(&$install_state) { } /** + * Creates configuration translations. + * + * @param $install_state + * An array of information about the current installation state. + * + * @return + * The batch definition, if there are configuration objects to update. + */ +function install_update_configuration_translations(&$install_state) { + include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc'; + 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/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index f232f63..efd7ce6 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -15,6 +15,13 @@ class InstallStorage extends FileStorage { /** + * Folder map indexed by configuration name + * + * @var array + */ + protected $folders; + + /** * Overrides Drupal\Core\Config\FileStorage::__construct(). */ public function __construct() { @@ -38,17 +45,9 @@ public function __construct() { * afterwards check for a corresponding module or theme. */ public function getFilePath($name) { - // Extract the owner. - $owner = strtok($name, '.'); - // Determine the path to the owner. - $path = FALSE; - foreach (array('profile', 'module', 'theme') as $type) { - if ($path = drupal_get_path($type, $owner)) { - $file = $path . '/config/' . $name . '.' . self::getFileExtension(); - if (file_exists($file)) { - return $file; - } - } + $folders = $this->getAllFolders(); + if (isset($folders[$name])) { + return $folders[$name] . '/' . $name . '.' . $this->getFileExtension(); } // If any code in the early installer requests a configuration object that // does not exist anywhere as default config, then that must be mistake. @@ -90,6 +89,74 @@ public function rename($name, $new_name) { * @throws Drupal\Core\Config\StorageException */ public function listAll($prefix = '') { - throw new StorageException('List operation is not allowed during install.'); + $names = array_keys($this->getAllFolders()); + if (!$prefix) { + return $names; + } + else { + foreach ($names as $index => $name) { + if (strpos($name, $prefix) !== 0 ) { + unset($names[$index]); + } + } + return $names; + } + } + + /** + * Returns a map of all metadata names and the module they belong to. + * + * @return array + * An array mapping metadata names with directories. + */ + protected function getAllFolders() { + if (!isset($this->folders)) { + $this->folders = $this->getComponentNames('profile', array(drupal_get_profile())); + $this->folders += $this->getComponentNames('module', module_list()); + $this->folders += $this->getComponentNames('theme', array_keys(list_themes())); + } + return $this->folders; + } + + /** + * Get all configuration names and folders for a list of modules or themes. + * + * @param string $type + * Type of components: 'module' | 'theme' | 'profile' + * @param array $list + * Array of theme or module names. + * + * @return array + * Folders indexed by configuration name. + */ + public function getComponentNames($type, $list) { + $extension = '.' . $this->getFileExtension(); + $folders = array(); + foreach ($list as $name) { + $directory = $this->getComponentFolder($type, $name); + if (file_exists($directory)) { + $files = glob($directory . '/*' . $extension); + foreach ($files as $filename) { + $name = basename($filename, $extension); + $folders[$name] = $directory; + } + } + } + return $folders; + } + + /** + * Get folder inside each component that contains the files. + * + * @param string $type + * Component type: 'module' | 'theme' | 'profile' + * @param string $name + * Component name. + * + * @return string + * The configuration folder name for this component. + */ + protected function getComponentFolder($type, $name) { + return drupal_get_path($type, $name) . '/config'; } } diff --git a/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php b/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php new file mode 100644 index 0000000..21e9cf2 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/ConfigWrapper.php @@ -0,0 +1,157 @@ +name = $name; + $this->data = $data; + } + + /** + * Gets the configuration language. + * + * @return Drupal\Core\Language\Language + * The language object. + */ + public function getLanguage() { + $meta = $this->getMetadata(); + // The default language will be English as this is hardcoded information. + $langcode = isset($meta['language']) ? $meta['language'] : 'en'; + return new Language(array('langcode' => $langcode)); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getKey(). + */ + public function getKey() { + // This is the parent wrapper, so there is no key. + return ''; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getMetadata(). + */ + public function getMetadata() { + return config_metadata($this->name); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getBase(). + */ + public function getBase() { + return $this->buildDefinition($this->data, $this->getMetadata()); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::get(). + */ + public function get($property_name) { + if ($property = $this->getElement($property_name)) { + return $property; + } + else { + throw new InvalidArgumentException(format_string("The configuration property @key doesn't exist.", array( + '@key' => $property_name, + ))); + } + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + // Set the data into the configuration array but behave according to the + // interface specification when we've got a null value. + if (isset($value)) { + $this->setElementData($property_name, $value); + return $this->get($property_name); + } + else { + // In these objects, when clearing the value, the property is gone. + // As this needs to return a property, we get it before we delete it. + $property = $this->get($property_name); + $this->clearElementData($property_name); + $property->setValue($value); + return $property; + } + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + return $this->toArray(); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + return $this->getData(); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + foreach ($values as $name => $value) { + $this->set($name, $value); + } + return $this; + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definition = $this->getElementDefinition($name); + return $definition ? $definition : FALSE; + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + return $this->getChildrenMetadata($this->getMetadata()); + } + + /** + * Implements Drupal\Core\TypedData\ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + return empty($this->data); + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/ListElement.php b/core/lib/Drupal/Core/Config/Metadata/ListElement.php new file mode 100644 index 0000000..595ad07 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/ListElement.php @@ -0,0 +1,67 @@ +getElementData($offset); + return isset($data); + } + + /** + * Implements ArrayAccess::offsetGet(). + */ + public function offsetGet($offset) { + return $this->buildElement($offset); + } + + /** + * Implements ArrayAccess::offsetSet(). + */ + public function offsetSet($offset, $value) { + if ($value instanceof TypedDataInterface) { + $value = $value->getValue(); + } + $this->setElementData($offset, $value); + } + + /** + * Implements ArrayAccess::offsetUnset(). + */ + public function offsetUnset($offset) { + $this->clearElementData($offset); + } + + /** + * Implements Countable::count(). + */ + public function count() { + return count($this->getData()); + } + + /** + * Implements Drupal\Core\TypedData\ListInterface::isEmpty(). + */ + public function isEmpty() { + return empty($this->data); + } +} diff --git a/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php b/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php new file mode 100644 index 0000000..48ac596 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/MetadataLookup.php @@ -0,0 +1,112 @@ +getBaseName($offset)) { + $metadata = $this->offsetGet($basename); + } + // Merge current metadata on top of it. + if ($meta = $this->readMetadata($offset)) { + $metadata = NestedArray::mergeDeep($metadata, $meta); + } + $this->storage[$offset] = $metadata; + return $metadata; + } + + /** + * Gets parent metadata name. + * + * @param string $name + * Configuration name or key. + * + * @return string + * Same name with the last part replaced by the filesystem marker. + */ + protected static function getBaseName($name) { + $parts = explode('.', $name); + $last = array_pop($parts); + // If this is not the generic metadata (where the last component of the + // name is a '%'), try to load that first. + if ($last != self::BASE_MARK) { + return implode('.', array_merge($parts, array(self::BASE_MARK))); + } + } + + /** + * Reads metadata from file system. + * + * @param string $name + * Configuration name or key. + * + * @return array + * Metadata array if found or empty array if not. + */ + protected function readMetadata($name) { + if (!isset($this->metadataStorage)) { + $this->metadataStorage = new MetadataStorage(); + } + try { + return $this->metadataStorage->read($name); + } + catch (Symfony\Component\Yaml\Exception\ParseException $e) { + // @todo Just log an error, but return empty array. + } + return array(); + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php b/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php new file mode 100644 index 0000000..5f16f41 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/MetadataStorage.php @@ -0,0 +1,31 @@ +getAllFolders()); + } + + /** + * Overrides Drupal\Core\Config\InstallStorage::getComponentFolder(). + */ + protected function getComponentFolder($type, $name) { + return drupal_get_path($type, $name) . '/meta'; + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/NestedElement.php b/core/lib/Drupal/Core/Config/Metadata/NestedElement.php new file mode 100644 index 0000000..b0cf279 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/NestedElement.php @@ -0,0 +1,175 @@ +definition = $definition; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setData(). + */ + public function setData($value) { + $this->data = $value; + $this->getParent()->setElementData($this->key, $this->data); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setElementData() + */ + public function setElementData($key, $value) { + parent::setElementData($key, $value); + $this->getParent()->setElementData($this->key, $this->data); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getKey(). + */ + public function getKey() { + $parent = $this->getParent()->getKey(); + return ($parent || is_numeric($parent) ? $parent . '.' : '') . $this->key; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getMetadata(). + */ + public function getMetadata() { + return $this->getParent()->getElementMetadata($this->key); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getBase(). + */ + public function getBase() { + return $this->getDefinition(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::clearElementData() + */ + public function clearElementData($key) { + parent::clearElementData($key); + $this->getParent()->setElementData($this->key, $data); + } + + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getType(). + */ + public function getType() { + return $this->definition['type']; + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getDefinition(). + */ + public function getDefinition() { + return $this->definition; + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getValue(). + */ + public function getValue() { + return $this->getData(); + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::setValue(). + */ + public function setValue($value) { + $this->data = $value; + // Due to how the constructor works, this function may get called before + // the parent is set. + // @see Drupal\Core\TypedData\TypedDataManager::createInstance() + if ($this->getParent()) { + $this->getParent()->set($this->key, $value); + } + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::getString(). + */ + public function getString() { + return (string) $this->getValue(); + } + + /** + * Implements Drupal\Core\TypedData\TypedDataInterface::validate(). + */ + public function validate() { + // This will be ok if we have any config data at all. + return isset($this->data); + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::getName(). + */ + public function getName() { + return $this->key; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->key = $name; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::getParent(). + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements Drupal\Core\TypedData\ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php b/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php new file mode 100644 index 0000000..e0b48c9 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/WrapperBase.php @@ -0,0 +1,293 @@ +getElementDefinition($key); + $value = $this->getElementData($key); + // If this is an end property, the value will be the configuration data. + // If it is a NestedElement, it won't be set, but retrieved from the parent + // element. See NestedElement::setValue() + $context = array('name' => $key, 'parent' => $this); + return typed_data()->create($definition, $value, $context); + } + + /** + * Builds metadata for a child element. + * + * @param $key + * Element's key. + * + * @return array + * Configuration metadata for the element. + */ + protected function buildElementMetadata($key) { + $metadata = $this->getMetadata(); + $meta = isset($metadata[$key]) ? $metadata[$key] : array(); + $definition = $this->getBase(); + if (isset($definition['elements.base']) && $base = config_metadata($definition['elements.base'])) { + // Load common base metadata for all elements. + $meta = NestedArray::mergeDeep($base, $meta); + } + if (isset($definition['elements.name'])) { + // Load specific metadata for each element. + $name = $definition['elements.name']; + $element_key = $key; + if (isset($definition['elements.key'])) { + // The search key for this one is in the data itself. + $key_name = str_replace('%', $key, $definition['elements.key']); + + $element_key = $this->getElementData($key_name); + } + if ($element_key) { + $name = str_replace('%', $element_key, $name); + } + if ($merge = config_metadata($name)) { + $meta = NestedArray::mergeDeep($meta, $merge); + } + } + if (isset($definition['elements.label']) && !isset($meta['.label'])) { + // Extract element's label from configuration data. + $key_name = str_replace('%', $key, $definition['elements.label']); + if ($label = $this->getElementData($key_name)) { + $meta['.label'] = $label; + } + } + return $meta; + } + + /** + * Builds element definition from metadata. + * + * @param mixed $data + * The configuration data for the element. + * + * @param array $metadata + * The raw metadata array for the element. + * + * @return array + * The elemement's data definition. + */ + protected function buildDefinition($data, $metadata) { + $element = $children = array(); + foreach ($metadata as $name => $value) { + if (strpos($name, '.') === 0) { + // Metadata properties will be prefixed by a dot. + $element[substr($name, 1)] = $value; + } + else { + // Children elements will be arrays of metadata themselves. + $children[$name] = $value; + } + } + // The default type will depend on whether we've got children or not. + $element['config.children'] = !empty($children); + if (!isset($element['type'])) { + $element['type'] = $element['config.children'] || is_array($data) ? 'config_list' : 'string'; + } + return $element; + } + + /** + * Gets valid configuration data keys. + * + * @return array + * Array of valid configuration data keys. + */ + protected function getAllKeys() { + return is_array($this->data) ? array_keys($this->data) : array(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getData(). + */ + public function getData() { + return $this->data; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setData(). + */ + public function setData($value) { + $this->data = $value; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getLabel(). + */ + public function getLabel() { + $definition = $this->getBase(); + return isset($definition['label']) ? $definition['label'] : $this->getKey(); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElement(). + */ + public function getElement($key) { + $parts = explode('.', $key); + $first = array_shift($parts); + $data = $this->getElementData($first); + // Only attempt to build the element if it actually has configuration data. + if (isset($data) && $element = $this->buildElement($first)) { + $subkey = implode('.', $parts); + // If not a nested key, return the element whatever it is. But if we need + // to find a nested key, this is only possible if it's a WrapperInterface. + if (!$subkey) { + return $element; + } + elseif ($element instanceof WrapperInterface) { + return $element->getElement($subkey); + } + } + return NULL; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::setElementData(). + */ + public function setElementData($key, $value) { + $parts = explode('.', $key); + if (count($parts) == 1) { + $this->data[$key] = $value; + } + else { + NestedArray::setValue($this->data, $parts, $value); + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementData(). + */ + public function getElementData($key) { + $parts = explode('.', $key); + if (count($parts) == 1) { + return isset($this->data[$key]) ? $this->data[$key] : NULL; + } + else { + $value = NestedArray::getValue($this->data, $parts, $key_exists); + return $key_exists ? $value : NULL; + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementDefinition(). + */ + public function getElementDefinition($key) { + return $this->buildDefinition($this->getElementData($key), $this->getElementMetadata($key)); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementMetadata(). + */ + public function getElementMetadata($key) { + // Metadata for nested elements must be built before. + if (!isset($this->metadata[$key])) { + $this->metadata[$key] = $this->buildElementMetadata($key); + } + return $this->metadata[$key]; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementLabel(). + */ + public function getElementLabel($key) { + $definition = $this->getElementDefinition($key); + return isset($definition['label']) ? $definition['label'] : $this->getElementKey($key); + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::getElementKey(). + */ + public function getElementKey($key) { + $parent = $this->getKey(); + return $parent ? $parent . '.' . $key : $key; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::clearElementData(). + */ + public function clearElementData($key) { + $parts = explode('.', $key); + if (count($parts) == 1) { + unset($this->data[$key]); + } + else { + NestedArray::unsetValue($this->data, $parts); + } + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::toArray(). + */ + public function toArray() { + $array = array(); + foreach ($this->getAllKeys() as $key) { + $array[$key] = $this->buildElement($key); + } + return $array; + } + + /** + * Implements Drupal\Core\Config\Metadata\WrapperInterface::toList(). + */ + public function toList($base_name = '') { + $list = array(); + foreach ($this->toArray() as $key => $element) { + $name = $base_name ? $base_name . '.' . $key : $key; + if ($element instanceof WrapperInterface) { + $list += $element->toList($name); + } + else { + $list[$name] = $element; + } + } + return $list; + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->toArray()); + } + +} diff --git a/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php b/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php new file mode 100644 index 0000000..2547453 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Metadata/WrapperInterface.php @@ -0,0 +1,213 @@ + 'Configuration metadata', + 'description' => 'Tests Metadata for configuration objects.', + 'group' => 'Configuration', + ); + } + + /** + * Tests the basic metadata retrieval layer. + */ + function testBasicMetadata() { + // Simple case, straight metadata. + $metadata = config_metadata('system.maintenance'); + $expected = array( + 'enabled' => array( + '.label' => 'Put site into maintenance mode', + '.type' => 'boolean' + ), + 'message' => array( + '.label' => 'Message to display when in maintenance mode', + '.type' => 'text', + ), + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for system.maintenance'); + // More complex case, fallback to parent name. + $metadata = config_metadata('image.style.large'); + $expected = array( + 'name' => array( + '.label' => 'Machine name', + '.type' => 'string', + ), + 'label' => array( + '.label' => 'Label', + '.type' => 'label' + ), + 'effects' => array( + '.label' => 'Style effects', + '.type' => 'config_list', + '.elements.name' => 'image.style.effects.%', + '.elements.key' => '%.name' + ) + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for image.style.large'); + + // Most complex case, fallback to parent name with merging + $metadata = config_metadata('image.style.effects.image_scale'); + $expected = array( + '.label' => 'Image scale', + 'name' => array( + '.label' => 'Style name', + '.type' => 'string', + ), + 'data' => array( + '.label' => 'Data', + '.type' => 'config_list', + 'width' => array( + '.label' => 'Width', + '.type' => 'integer' + ), + 'height' => array( + '.label' => 'Height', + '.type' => 'integer', + ), + 'upscale' => array( + '.label' => 'Upscale', + '.type' => 'boolean', + ) + ), + 'weight' => array( + '.label' => 'Weight', + '.type' => 'integer' + ), + 'ieid' => array( + '.label' => 'IEID', + ), + + ); + $this->assertEqual($metadata, $expected, 'Retrieved the right metadata for image.style.effects.scale'); + + } + + /** + * Tests metadata applied to configuration objects. + */ + function testConfigMetadata() { + // Try some simple properties. + $meta = config_wrapper('system.site'); + $property = $meta->get('name'); + $this->assertTrue(is_a($property, 'Drupal\Core\TypedData\Type\String'), 'Got the right wrapper fo the site name property.'); + $this->assertEqual($property->getType(), 'label', 'Got the right string type for site name data.'); + $this->assertEqual($property->getValue(), 'Drupal', 'Got the right string value for site name data.'); + + $property = $meta->get('page.front'); + $this->assertTrue(is_a($property, 'Drupal\Core\TypedData\Type\String'), 'Got the right wrapper fo the page.front property.'); + $this->assertEqual($property->getType(), 'string', 'Got the right type for page.front data (undefined).'); + $this->assertEqual($property->getValue(), 'user', 'Got the right value for page.front data.'); + + // Check nested array of properties. + $list = $meta->get('page'); + $this->assertEqual(count($list), 3, 'Got a list with the right number of properties for site page data'); + $this->assertTrue(isset($list['front']) && isset($list['403']) && isset($list['404']), 'Got a list with the right properties for site page data.'); + $this->assertEqual($list['front']->getValue(), 'user', 'Got the right value for page.front data from the list.'); + + // Now let's try something more complex, with nested objects. + $wrapper = config_wrapper('image.style.large'); + $effects = $wrapper->get('effects'); + // The function is_array() doesn't work with ArrayAccess, so we use count(). + $this->assertTrue(count($effects) == 1, 'Got an array with effects for image.style.large data'); + $ieid = key($effects->toArray()); + $effect = $wrapper->get('effects.' . $ieid); + $this->assertTrue(count($effect['data']) && $effect['name']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.'); + $this->assertEqual($effect['data']['width']->getType(), 'integer', 'Got the right type for the scale effect width.'); + $this->assertEqual($effect['data']['width']->getValue(), 480, 'Got the right value for the scale effect width.' ); + // Try config key and getting nested properties. + $key = 'effects.' . $ieid . '.data'; + $this->assertEqual($effect['data']->getKey(), $key, 'Got the right configuration key for a nested image styple effect'); + $object = $wrapper->get($key); + $this->assertEqual(get_class($effect['data']), get_class($object), 'Got the same object class using different keys with dot notation.'); + $this->assertEqual($effect['data']->getData(), $object->getData(), 'Got the same object values using different keys with dot notation.'); + + // Finally update some object using a configuration wrapper. + $new_slogan = 'Site slogan for testing configuration metadata'; + $wrapper = config_wrapper('system.site'); + $wrapper->set('slogan', $new_slogan); + $site_slogan = $wrapper->get('slogan'); + $this->assertEqual($site_slogan->getValue(), $new_slogan, 'Successfully updated the contained configuration data'); + } + +} diff --git a/core/modules/contact/meta/contact.category.%.yml b/core/modules/contact/meta/contact.category.%.yml new file mode 100644 index 0000000..a24e3a7 --- /dev/null +++ b/core/modules/contact/meta/contact.category.%.yml @@ -0,0 +1,15 @@ +id: + .type: string + .label: 'Id' +label: + .type: label + .label: 'Label' +recipients: + .type: config_list + .label: 'Recipients' +reply: + .type: text + .label: 'Reply' +weight: + .type: integer + .label: 'Weight' diff --git a/core/modules/image/meta/image.style.%.yml b/core/modules/image/meta/image.style.%.yml new file mode 100644 index 0000000..99b54ba --- /dev/null +++ b/core/modules/image/meta/image.style.%.yml @@ -0,0 +1,11 @@ +name: + .label: 'Machine name' + .type: string +label: + .label: 'Label' + .type: label +effects: + .label: 'Style effects' + .type: config_list + .elements.name: 'image.style.effects.%' + .elements.key: '%.name' diff --git a/core/modules/image/meta/image.style.effects.%.yml b/core/modules/image/meta/image.style.effects.%.yml new file mode 100644 index 0000000..7191867 --- /dev/null +++ b/core/modules/image/meta/image.style.effects.%.yml @@ -0,0 +1,11 @@ +name: + .label: 'Style name' + .type: string +data: + .label: 'Data' + .type: config_list +weight: + .label: 'Weight' + .type: integer +ieid: + .label: 'IEID' diff --git a/core/modules/image/meta/image.style.effects.image_scale.yml b/core/modules/image/meta/image.style.effects.image_scale.yml new file mode 100644 index 0000000..30b017e --- /dev/null +++ b/core/modules/image/meta/image.style.effects.image_scale.yml @@ -0,0 +1,11 @@ +.label: 'Image scale' +data: + width: + .label: 'Width' + .type: 'integer' + height: + .label: 'Height' + .type: 'integer' + upscale: + .label: 'Upscale' + .type: 'boolean' diff --git a/core/modules/locale/lib/Drupal/locale/LocaleConfigSubscriber.php b/core/modules/locale/lib/Drupal/locale/LocaleConfigSubscriber.php index 3d2fd4a..fb09c0b 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleConfigSubscriber.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleConfigSubscriber.php @@ -18,6 +18,7 @@ * $config is always a DrupalConfig object. */ class LocaleConfigSubscriber implements EventSubscriberInterface { + /** * Override configuration values with localized data. * diff --git a/core/modules/locale/lib/Drupal/locale/LocaleConfigWrapper.php b/core/modules/locale/lib/Drupal/locale/LocaleConfigWrapper.php new file mode 100644 index 0000000..5a90d57 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/LocaleConfigWrapper.php @@ -0,0 +1,255 @@ +localeStorage = $localeStorage; + } + + /** + * Sets translation parameters. + * + * @param string $langcode + * The language code for the translation. + * @param Drupal\Core\Config\Metadata\ConfigWrapper $source + * Configuration wrapper used as translation source. + */ + public function setTranslation($langcode, $source) { + $this->translationLangcode = $langcode; + $this->translationSource = $source; + return $this; + } + + /** + * 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; + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). + */ + public function getTranslationLanguages($include_default = TRUE) { + $languages = locale_translatable_language_list(); + $default = $this->language(); + if ($include_default) { + $languages[$default->langcode] = $default; + } + else { + unset($language[$default->langcode]); + } + return $languages; + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::getTranslation(). + */ + public function getTranslation($langcode, $strict = TRUE) { + if (isset($this->translationSource)) { + return $this->translationSource->getTranslation($langcode, $strict); + } + else { + $options = array( + 'source' => $this->language()->langcode, + 'target' => $langcode, + 'strict' => $strict, + ); + //$data = $source->getTranslatedData($this, $options); + $data = $this->getTranslatedData($this->toArray(), $options); + $translation = new LocaleConfigWrapper($this->name, $data); + $translation->setTranslation($langcode, $strict, $this); + return $translation; + } + } + + /** + * Implements Drupal\Core\TypedData\TranslatableInterface::language(). + */ + public function language() { + if (isset($this->translationLangcode)) { + return new Language(array('langcode' => $this->translationLangcode)); + } + else { + return $this->getLanguage(); + } + } + + /** + * Get translated configuration data. + * + * @param array $elements + * Array of 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 WrapperInterface) { + $value = $this->getTranslatedData($element->toArray(), $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. + * + * @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. + */ + public function translateElement($element, $options) { + if ($this->canTranslate($options['source'], $options['target'])) { + $definition = $element->getDefinition(); + $value = $element->getValue(); + if ($value && is_string($value) && !empty($definition['translatable'])) { + $context = isset($definition['string_context']) ? $definition['string_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 $langcode + * Language code to translate to. + * @param string $source + * The source string. + * @param string $context + * The string context. + * + * @return string|FALSE + * 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/StringBase.php b/core/modules/locale/lib/Drupal/locale/StringBase.php index e2d9bcc..47d5acd 100644 --- a/core/modules/locale/lib/Drupal/locale/StringBase.php +++ b/core/modules/locale/lib/Drupal/locale/StringBase.php @@ -189,7 +189,7 @@ public function save() { } else { throw new StringStorageException(format_string('The string cannot be saved because its not bound to a storage: @string', array( - '@string' => $string->getString() + '@string' => $this->getString() ))); } return $this; 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..40c27fc --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigTranslationTest.php @@ -0,0 +1,78 @@ + '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')); + $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')); + + // Check site name string exists and create translation for it. + $site_name = $this->randomName(20); + $string = $this->storage->findString(array('source' => 'Drupal', 'context' => '', 'type' => 'configuration')); + $this->assertTrue($string, 'Configuration strings have been created upon installation.'); + $this->storage + ->createTranslation(array('lid' => $string->lid, 'language' => $langcode, 'translation' => $site_name)) + ->save(); + + $data = config('system.site')->get(); + $wrapper = new LocaleConfigWrapper('system.site', $data, $this->storage); + + // Get strict translation and check we've got only the site name. + $translation = $wrapper->getTranslation($langcode, TRUE); + $properties = $translation->toList(); + $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->toList(); + $this->assertEqual(count($properties), 6, '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'); + } + +} diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 0be2aec..8e01943 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -6,9 +6,13 @@ */ use Drupal\Component\Gettext\PoStreamWriter; +use Drupal\Component\Utility\NestedArray; +use Drupal\locale\LocaleConfigWrapper; use Drupal\locale\Gettext; use Drupal\locale\PoDatabaseReader; use Drupal\Core\Language\Language; +use Drupal\Core\Config\InstallStorage; +use Drupal\Core\Config\StorageException; /** @@ -109,7 +113,6 @@ function locale_translate_import_form($form, &$form_state) { function locale_translate_import_form_submit($form, &$form_state) { // Ensure we have the file uploaded. if ($file = file_save_upload('file', $form['file']['#upload_validators'], 'translations://')) { - // Add language, if not yet supported. $language = language_load($form_state['values']['langcode']); if (empty($language)) { @@ -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, ); $batch = locale_translate_batch_build(array($file->uri => $file), $options); batch_set($batch); @@ -290,6 +294,10 @@ function locale_translate_add_language_set_batch($options) { if ($batch = locale_translate_batch_import_files($options)) { batch_set($batch); } + // Create or update all configuration translations for this language. + if ($batch = locale_config_batch_update_components($options)) { + batch_set($batch); + } } /** @@ -389,8 +397,12 @@ function locale_translate_get_interface_translation_files($langcode = NULL) { * 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. * * @return * A batch structure or FALSE if $files was empty. @@ -400,6 +412,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)) { @@ -408,12 +421,14 @@ function locale_translate_batch_build($files, $options) { // We call locale_translate_batch_import for every batch operation. $operations[] = array('locale_translate_batch_import', array($file->uri, $options)); } + // Add a final step to refresh JavaScript and configuration strings. + $operations[] = array('locale_translate_batch_refresh', array($options)); $batch = array( - 'operations' => $operations, - 'title' => $t('Importing interface translations'), - 'init_message' => $t('Starting import'), - 'error_message' => $t('Error importing interface translations'), - 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc', + 'operations' => $operations, + 'title' => $t('Importing interface translations'), + 'init_message' => $t('Starting import'), + 'error_message' => $t('Error importing interface translations'), + 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc', ); if ($options['finish_feedback']) { $batch['finished'] = 'locale_translate_batch_finished'; @@ -517,12 +532,57 @@ function locale_translate_batch_import($filepath, $options, &$context) { } /** + * Refresh translations after importing strings. + */ +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_string_names($strings)) { + $context['sandbox']['refresh']['names'] = $names; + $context['sandbox']['refresh']['languages'] = $langcodes; + $context['sandbox']['refresh']['count'] = count($names); + $context['results']['stats']['configuration'] = 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']['configuration'] += $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; - $strings = $langcodes = array(); + $additions = $updates = $deletes = $skips = $configuration = 0; drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); $skipped_files = array(); // If there are no results and/or no stats (eg. coping with an empty .po @@ -536,11 +596,7 @@ function locale_translate_batch_finished($success, $results) { if ($report['skips'] > 0) { $skipped_files[] = $filepath; } - $strings = array_merge($strings, $report['strings']); } - // Get list of unique string identifiers and language codes updated. - $strings = array_unique($strings); - $langcodes = array_unique(array_values($results['languages'])); } drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); watchdog('locale', 'The translation was succesfully imported. %number new strings added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); @@ -554,10 +610,9 @@ function locale_translate_batch_finished($success, $results) { drupal_set_message($skip_message, 'error'); watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); } - - if ($strings) { - // Clear cache and force refresh of JavaScript translations. - _locale_refresh_translations($langcodes, $strings); + // Merge feedback about configuration updates too. + if (isset($results['stats']['configuration'])) { + locale_config_batch_finished($success, $results); } } } @@ -627,3 +682,230 @@ function locale_translate_delete_translation_files($langcode) { } return $return; } + +/** + * Build a locale batch to refresh configuration. + * + * @param array $options + * An array with options that can have the following elements: + * - 'langcode': The language code. Optional, defaults to NULL, which means + * that all translatable languages will be refreshed. + * - 'finish_feedback': Whether or not to give feedback to the user when the + * batch is finished. Optional, defaults to TRUE. + * @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. + */ +function locale_config_batch_update_components($options, $components = array()) { + $langcodes = isset($options['langcode']) ? array($options['langcode']) : array_keys(locale_translatable_language_list()); + if ($langcodes && $names = _locale_config_component_names($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. + */ +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 $context + * Contains a list of files imported. + */ +function locale_config_batch_refresh_name($name, $langcodes, &$context) { + if (!isset($context['result']['stats']['configuration'])) { + $context['result']['stats']['configuration'] = 0; + } + $context['result']['stats']['configuration'] += 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. + */ +function locale_config_batch_finished($success, $results) { + if ($success) { + $configuration = isset($results['stats']['configuration']) ? $results['stats']['configuration'] : 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); + } + } +} + +/** + * Delete configuration for language. + * + * @param $langcode + * Language code to delete. + */ +function locale_config_delete_language($langcode) { + $locale_name = 'locale.config.' . $langcode; + $storage = drupal_container()->get('config.storage'); + foreach ($storage->listAll($locale_name) as $name) { + $storage->delete($name); + } +} + +/** + * 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()) { + $default_storage = new InstallStorage(); + $config_storage = drupal_container()->get('config.storage'); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $count = 0; + foreach ($names as $name) { + $wrapper = NULL; + try { + if ($data = $default_storage->read($name)) { + $updated = $config_storage->read($name); + if ($data = _locale_config_data_compare($data, $updated)) { + $wrapper = new LocaleConfigWrapper($name, $data, locale_storage()); + } + } + foreach ($langcodes as $langcode) { + $locale_name = 'locale.config.' . $langcode . '.' . $name; + $translation = $wrapper ? $wrapper->getTranslation($langcode, TRUE)->getData() : NULL; + if ($translation) { + $config_storage->write($locale_name, $translation); + $count++; + } + else { + $config_storage->delete($locale_name); + } + } + } + catch (StorageException $e) { + // The configuration name does not exist, just continue. + } + } + return $count; +} + +/** + * 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. + */ +function _locale_config_data_compare($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] = _locale_config_data_compare($value, $updated[$key]); + } + elseif ($value === $updated[$key]) { + $result[$key] = $value; + } + } + } + return $result; +} + +/** + * Delete configuration for uninstalled components. + */ +function locale_config_delete_components($components) { + $names = _locale_config_component_names($components); + $langcodes = array_keys(locale_translatable_language_list()); + if ($names && $langcodes) { + $storage = drupal_container()->get('config.storage'); + foreach ($names as $name) { + foreach ($langcodes as $langcode) { + $storage->delete('locale.config.' . $langcode . '.' . $name); + } + } + } +} + +/** + * Get configuration names associated with components. + * + * @param array $components + * Array with string identifiers. + * + * @return array + * Array of configuration object names. + */ +function _locale_config_component_names($components) { + $storage = new InstallStorage(); + if ($components) { + $names = array(); + foreach ($components as $type => $list) { + $names = array_merge($names, $storage->getComponentNames($type, $list)); + } + return $names; + } + else { + return $storage->listAll(); + } +} + +/** + * Get configuration names associated with strings. + * + * @param array $lids + * Array with string identifiers. + * + * @return array + * Array of configuration object names. + */ +function _locale_config_string_names($lids) { + $names = array(); + $locations = locale_storage()->getLocations(array('sid' => $lids, 'type' => 'configuration')); + foreach ($locations as $location) { + $names[$location->name] = $location->name; + } + return $names; +} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 3ebe97d..b4771a0 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -37,27 +37,27 @@ * which is captured. Will fail if there are nested objects. */ define('LOCALE_JS_OBJECT_CONTEXT', ' - \{ # match object literal start - .*? # match anything, non-greedy - (?: # match a form of "context" + \{ # match object literal start + .*? # match anything, non-greedy + (?: # match a form of "context" \'context\' | "context" | context - ) - \s*:\s* # match key-value separator ":" - (' . LOCALE_JS_STRING . ') # match context string - .*? # match anything, non-greedy - \} # match end of object literal -'); + ) + \s*:\s* # match key-value separator ":" + (' . LOCALE_JS_STRING . ') # match context string + .*? # match anything, non-greedy + \} # match end of object literal + '); /** * Flag for locally not customized interface translation. * * Such translations are imported from .po files downloaded from * localize.drupal.org for example. - */ +*/ const LOCALE_NOT_CUSTOMIZED = 0; /** @@ -146,49 +146,49 @@ function locale_help($path, $arg) { function locale_menu() { // Translation functionality. $items['admin/config/regional/translate'] = array( - 'title' => 'User interface translation', - 'description' => 'Translate the built-in user interface.', - 'page callback' => 'locale_translate_page', - 'access arguments' => array('translate interface'), - 'file' => 'locale.pages.inc', - 'weight' => -5, + 'title' => 'User interface translation', + 'description' => 'Translate the built-in user interface.', + 'page callback' => 'locale_translate_page', + 'access arguments' => array('translate interface'), + 'file' => 'locale.pages.inc', + 'weight' => -5, ); $items['admin/config/regional/translate/translate'] = array( - 'title' => 'Translate', - 'weight' => -10, - 'type' => MENU_DEFAULT_LOCAL_TASK, + 'title' => 'Translate', + 'weight' => -10, + 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/config/regional/translate/import'] = array( - 'title' => 'Import', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_translate_import_form'), - 'access arguments' => array('translate interface'), - 'weight' => 20, - 'type' => MENU_LOCAL_TASK, - 'file' => 'locale.bulk.inc', + 'title' => 'Import', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_import_form'), + 'access arguments' => array('translate interface'), + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'file' => 'locale.bulk.inc', ); $items['admin/config/regional/translate/export'] = array( - 'title' => 'Export', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('locale_translate_export_form'), - 'access arguments' => array('translate interface'), - 'weight' => 30, - 'type' => MENU_LOCAL_TASK, - 'file' => 'locale.bulk.inc', + 'title' => 'Export', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_export_form'), + 'access arguments' => array('translate interface'), + 'weight' => 30, + 'type' => MENU_LOCAL_TASK, + 'file' => 'locale.bulk.inc', ); $items['admin/reports/translations'] = array( - 'title' => 'Available translation updates', - 'description' => 'Get a status report about available interface translations for your installed modules and themes.', - 'page callback' => 'locale_translation_status', - 'access arguments' => array('translate interface'), - 'file' => 'locale.pages.inc', + 'title' => 'Available translation updates', + 'description' => 'Get a status report about available interface translations for your installed modules and themes.', + 'page callback' => 'locale_translation_status', + 'access arguments' => array('translate interface'), + 'file' => 'locale.pages.inc', ); $items['admin/reports/translations/check'] = array( - 'title' => 'Manual translation update check', - 'page callback' => 'locale_translation_manual_status', - 'access arguments' => array('translate interface'), - 'type' => MENU_CALLBACK, - 'file' => 'locale.pages.inc', + 'title' => 'Manual translation update check', + 'page callback' => 'locale_translation_manual_status', + 'access arguments' => array('translate interface'), + 'type' => MENU_CALLBACK, + 'file' => 'locale.pages.inc', ); return $items; @@ -199,10 +199,10 @@ function locale_menu() { */ function locale_permission() { return array( - 'translate interface' => array( - 'title' => t('Translate interface texts'), - 'restrict access' => TRUE, - ), + 'translate interface' => array( + 'title' => t('Translate interface texts'), + 'restrict access' => TRUE, + ), ); } @@ -211,10 +211,10 @@ function locale_permission() { */ function locale_theme() { return array( - 'locale_translate_edit_form_strings' => array( - 'render element' => 'form', - 'file' => 'locale.pages.inc', - ), + 'locale_translate_edit_form_strings' => array( + 'render element' => 'form', + 'file' => 'locale.pages.inc', + ), ); } @@ -223,12 +223,12 @@ function locale_theme() { */ function locale_stream_wrappers() { $wrappers = array( - 'translations' => array( - 'name' => t('Translation files'), - 'class' => 'Drupal\locale\TranslationsStream', - 'description' => t('Translation files'), - 'type' => STREAM_WRAPPERS_LOCAL_NORMAL, - ), + 'translations' => array( + 'name' => t('Translation files'), + 'class' => 'Drupal\locale\TranslationsStream', + 'description' => t('Translation files'), + 'type' => STREAM_WRAPPERS_LOCAL_NORMAL, + ), ); return $wrappers; } @@ -265,6 +265,7 @@ function locale_language_delete($language) { // Remove interface translation files. module_load_include('inc', 'locale', 'locale.bulk'); locale_translate_delete_translation_files($language->langcode); + locale_config_delete_language($language->langcode); _locale_invalidate_js($language->langcode); @@ -415,12 +416,21 @@ function locale_get_plural($count, $langcode = NULL) { return $plural_indexes[$langcode][$count]; } - /** * 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) { + include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc'; + $components['module'] = $modules; + locale_config_delete_components($components); } /** @@ -430,7 +440,8 @@ function locale_modules_installed($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); } /** @@ -440,8 +451,8 @@ function locale_themes_enabled($themes) { * right away, or start a batch if more files need to be imported. * * @param $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. * * @todo * This currently imports all .po files available, independent of @@ -458,6 +469,9 @@ function locale_system_update($components) { if ($batch = locale_translate_batch_import_files(array(), TRUE)) { batch_set($batch); } + if ($batch = locale_config_batch_update_components(array(), $components)) { + batch_set($batch); + } } } @@ -528,28 +542,28 @@ function locale_js_alter(&$javascript) { */ function locale_library_info() { $libraries['drupal.locale.admin'] = array( - 'title' => 'Locale', - 'version' => VERSION, - 'js' => array( - drupal_get_path('module', 'locale') . '/locale.admin.js' => array(), - ), - 'dependencies' => array( - array('system', 'jquery'), - array('system', 'drupal'), - array('system', 'jquery.once'), - ), + 'title' => 'Locale', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'locale') . '/locale.admin.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + ), ); $libraries['drupal.locale.datepicker'] = array( - 'title' => 'Locale Datepicker UI', - 'version' => VERSION, - 'js' => array( - drupal_get_path('module', 'locale') . '/locale.datepicker.js' => array(), - ), - 'dependencies' => array( - array('system', 'jquery'), - array('system', 'drupal'), - array('system', 'drupalSettings'), - ), + 'title' => 'Locale Datepicker UI', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'locale') . '/locale.datepicker.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + ), ); return $libraries; @@ -568,17 +582,17 @@ function locale_library_info_alter(&$libraries, $module) { // hook_library_info_alter(), thus does not have to be specified explicitly. $libraries['jquery.ui.datepicker']['dependencies'][] = array('locale', 'drupal.locale.datepicker'); $libraries['jquery.ui.datepicker']['js'][] = array( - 'data' => array( - 'jquery' => array( - 'ui' => array( - 'datepicker' => array( - 'isRTL' => $language_interface->direction == LANGUAGE_RTL, - 'firstDay' => variable_get('date_first_day', 0), + 'data' => array( + 'jquery' => array( + 'ui' => array( + 'datepicker' => array( + 'isRTL' => $language_interface->direction == LANGUAGE_RTL, + 'firstDay' => variable_get('date_first_day', 0), + ), + ), ), - ), ), - ), - 'type' => 'setting', + 'type' => 'setting', ); } } @@ -607,25 +621,25 @@ function locale_form_language_admin_overview_form_alter(&$form, &$form_state) { foreach ($languages as $langcode => $language) { $stats[$langcode] += array( - 'translated' => 0, - 'ratio' => 0, + 'translated' => 0, + 'ratio' => 0, ); if (!$language->locked && ($langcode != 'en' || locale_translate_english())) { $form['languages'][$langcode]['locale_statistics'] = array( - '#markup' => l( - t('@translated/@total (@ratio%)', array( - '@translated' => $stats[$langcode]['translated'], - '@total' => $total_strings, - '@ratio' => $stats[$langcode]['ratio'], - )), - 'admin/config/regional/translate/translate', - array('query' => array('langcode' => $langcode)) - ), + '#markup' => l( + t('@translated/@total (@ratio%)', array( + '@translated' => $stats[$langcode]['translated'], + '@total' => $total_strings, + '@ratio' => $stats[$langcode]['ratio'], + )), + 'admin/config/regional/translate/translate', + array('query' => array('langcode' => $langcode)) + ), ); } else { $form['languages'][$langcode]['locale_statistics'] = array( - '#markup' => t('not applicable'), + '#markup' => t('not applicable'), ); } } @@ -662,9 +676,9 @@ function locale_form_language_admin_add_form_alter_submit($form, $form_state) { function locale_form_language_admin_edit_form_alter(&$form, &$form_state) { if ($form['langcode']['#type'] == 'value' && $form['langcode']['#value'] == 'en') { $form['locale_translate_english'] = array( - '#title' => t('Enable interface translation to English'), - '#type' => 'checkbox', - '#default_value' => locale_translate_english(), + '#title' => t('Enable interface translation to English'), + '#type' => 'checkbox', + '#default_value' => locale_translate_english(), ); $form['#submit'][] = 'locale_form_language_admin_edit_form_alter_submit'; } @@ -694,13 +708,13 @@ function locale_translate_english() { */ function locale_form_system_file_system_settings_alter(&$form, $form_state) { $form['locale_translate_file_directory'] = array( - '#type' => 'textfield', - '#title' => t('Interface translations directory'), - '#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'), - '#maxlength' => 255, - '#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'), - '#after_build' => array('system_check_directory'), - '#weight' => 10, + '#type' => 'textfield', + '#title' => t('Interface translations directory'), + '#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'), + '#maxlength' => 255, + '#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'), + '#after_build' => array('system_check_directory'), + '#weight' => 10, ); if ($form['file_default_scheme']) { $form['file_default_scheme']['#weight'] = 20; @@ -794,6 +808,27 @@ function _locale_refresh_translations($langcodes, $lids) { } /** + * 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) { + include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc'; + if ($names = _locale_config_string_names($lids)) { + 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. * @@ -814,24 +849,24 @@ function _locale_parse_js_file($filepath) { // Match all calls to Drupal.t() in an array. // Note: \s also matches newlines with the 's' modifier. preg_match_all('~ - [^\w]Drupal\s*\.\s*t\s* # match "Drupal.t" with whitespace - \(\s* # match "(" argument list start - (' . LOCALE_JS_STRING . ')\s* # capture string argument - (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture str args - (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context - ?)? # close optional args - [,\)] # match ")" or "," to finish - ~sx', $file, $t_matches); + [^\w]Drupal\s*\.\s*t\s* # match "Drupal.t" with whitespace + \(\s* # match "(" argument list start + (' . LOCALE_JS_STRING . ')\s* # capture string argument + (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture str args + (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context + ?)? # close optional args + [,\)] # match ")" or "," to finish + ~sx', $file, $t_matches); // Match all Drupal.formatPlural() calls in another array. preg_match_all('~ - [^\w]Drupal\s*\.\s*formatPlural\s* # match "Drupal.formatPlural" with whitespace - \( # match "(" argument list start - \s*.+?\s*,\s* # match count argument - (' . LOCALE_JS_STRING . ')\s*,\s* # match singular string argument - ( # capture plural string argument - (?: # non-capturing group to repeat string pieces - (?: + [^\w]Drupal\s*\.\s*formatPlural\s* # match "Drupal.formatPlural" with whitespace + \( # match "(" argument list start + \s*.+?\s*,\s* # match count argument + (' . LOCALE_JS_STRING . ')\s*,\s* # match singular string argument + ( # capture plural string argument + (?: # non-capturing group to repeat string pieces + (?: \' # match start of single-quoted string (?:\\\\\'|[^\'])* # match any character except unescaped single-quote @count # match "@count" @@ -843,38 +878,38 @@ function _locale_parse_js_file($filepath) { @count # match "@count" (?:\\\\"|[^"])* # match any character except unescaped double-quote " # match end of double-quoted string - ) - (?:\s*\+\s*)? # match "+" with possible whitespace, for str concat - )+ # match multiple because we supports concatenating strs - )\s* # end capturing of plural string argument - (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture string args - (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)? # optionally capture context - )? - [,\)] - ~sx', $file, $plural_matches); + ) + (?:\s*\+\s*)? # match "+" with possible whitespace, for str concat + )+ # match multiple because we supports concatenating strs + )\s* # end capturing of plural string argument + (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture string args + (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)? # optionally capture context + )? + [,\)] + ~sx', $file, $plural_matches); $matches = array(); // Add strings from Drupal.t(). foreach ($t_matches[1] as $key => $string) { $matches[] = array( - 'string' => $string, - 'context' => $t_matches[2][$key], + 'string' => $string, + 'context' => $t_matches[2][$key], ); } // Add string from Drupal.formatPlural(). foreach ($plural_matches[1] as $key => $string) { $matches[] = array( - 'string' => $string, - 'context' => $plural_matches[3][$key], + 'string' => $string, + 'context' => $plural_matches[3][$key], ); // If there is also a plural version of this string, add it to the strings array. if (isset($plural_matches[2][$key])) { $matches[] = array( - 'string' => $plural_matches[2][$key], - 'context' => $plural_matches[3][$key], + 'string' => $plural_matches[2][$key], + 'context' => $plural_matches[3][$key], ); } } @@ -891,8 +926,8 @@ function _locale_parse_js_file($filepath) { if (!$source) { // We don't have the source string yet, thus we insert it into the database. $source = locale_storage()->createString(array( - 'source' => $string, - 'context' => $context, + 'source' => $string, + 'context' => $context, )); } // Besides adding the location this will tag it for current version. @@ -953,9 +988,9 @@ function _locale_rebuild_js($langcode = NULL) { // Construct the array for JavaScript translations. // Only add strings with a translation to the translations array. $conditions = array( - 'type' => 'javascript', - 'language' => $language->langcode, - 'translated' => TRUE, + 'type' => 'javascript', + 'language' => $language->langcode, + 'translated' => TRUE, ); $translations = array(); foreach (locale_storage()->getTranslations($conditions) as $data) { diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 8ea8780..7ca6e17 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -406,7 +406,6 @@ function locale_translate_edit_form_submit($form, &$form_state) { foreach (locale_storage()->getTranslations(array('lid' => $lids, 'language' => $langcode, 'translated' => TRUE)) as $string) { $strings[$string->lid] = $string; } - foreach ($form_state['values']['strings'] as $lid => $translations) { // No translation when all strings are empty. $has_translation = FALSE; @@ -439,7 +438,8 @@ 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_configuration(array($langcode), $updated); _locale_refresh_translations(array($langcode), $updated); } diff --git a/core/modules/system/meta/system.maintenance.yml b/core/modules/system/meta/system.maintenance.yml new file mode 100644 index 0000000..96be6e2 --- /dev/null +++ b/core/modules/system/meta/system.maintenance.yml @@ -0,0 +1,6 @@ +enabled: + .label: 'Put site into maintenance mode' + .type: boolean +message: + .label: 'Message to display when in maintenance mode' + .type: text diff --git a/core/modules/system/meta/system.site.yml b/core/modules/system/meta/system.site.yml new file mode 100644 index 0000000..7917269 --- /dev/null +++ b/core/modules/system/meta/system.site.yml @@ -0,0 +1,17 @@ +name: + .label: 'Site name' + .type: label +mail: + .label: 'Site mail' +slogan: + .label: 'Site slogan' + .type: text +page: + .type: config_list + .label: 'Default pages' + 403: + .label: 'Default 403 (access denied) page' + 404: + .label: 'Default 404 (not found) page' + front: + .label: 'Default front page' diff --git a/core/modules/system/system.module b/core/modules/system/system.module index a9d951b..b951cdc 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2090,6 +2090,31 @@ function system_data_type_info() { 'class' => '\Drupal\Core\Entity\Field\Type\EntityReferenceItem', 'list class' => '\Drupal\Core\Entity\Field\Type\Field', ), + 'config_list' => array( + 'label' => t('Configuration list'), + 'description' => t('List of configuration data'), + 'class' => 'Drupal\Core\Config\Metadata\ListElement', + ), + 'config_element' => array( + 'label' => t('Nested configuration'), + 'description' => t('Nested configuration data'), + 'class' => 'Drupal\Core\Config\Metadata\NestedElement', + ), + // These are UI string types. + 'label' => array( + 'label' => t('Label'), + 'description' => t('Short object name.'), + 'class' => '\Drupal\Core\TypedData\Type\String', + 'primitive type' => Primitive::STRING, + 'translatable' => TRUE, + ), + 'text' => array( + 'label' => t('Text'), + 'description' => t('Textual content.'), + 'class' => '\Drupal\Core\TypedData\Type\String', + 'primitive type' => Primitive::STRING, + 'translatable' => TRUE, + ), ); } diff --git a/core/modules/user/meta/user.mail.%.yml b/core/modules/user/meta/user.mail.%.yml new file mode 100644 index 0000000..11b479b --- /dev/null +++ b/core/modules/user/meta/user.mail.%.yml @@ -0,0 +1,7 @@ +.label: 'Mail text' +subject: + .label: 'Subject' + .type: 'text' +body: + .label: 'Body' + .type: 'text' diff --git a/core/modules/user/meta/user.mail.yml b/core/modules/user/meta/user.mail.yml new file mode 100644 index 0000000..35cbd52 --- /dev/null +++ b/core/modules/user/meta/user.mail.yml @@ -0,0 +1,3 @@ +.label: 'User mails' +.type: config_list +.elements.name: 'user.mail.%'