Commit 91fc0d3e authored by alexpott's avatar alexpott

Issue #1905152 by Gábor Hojtsy, Jose Reyero, vijaycs85, YesCT, penyaskito:...

Issue #1905152 by Gábor Hojtsy, Jose Reyero, vijaycs85, YesCT, penyaskito: Integrate config schema with locale, so shipped configuration is translated.
parent ae0984f8
......@@ -86,6 +86,8 @@ services:
arguments: ['@database', config_snapshot]
config.storage.schema:
class: Drupal\Core\Config\Schema\SchemaStorage
config.storage.installer:
class: Drupal\Core\Config\InstallStorage
config.typed:
class: Drupal\Core\Config\TypedConfigManager
arguments: ['@config.storage', '@config.storage.schema']
......
......@@ -760,6 +760,12 @@ function install_tasks($install_state) {
'type' => 'batch',
'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP,
),
'install_update_configuration_translations' => array(
'display_name' => st('Translate configuration'),
'display' => $needs_translations,
'type' => 'batch',
'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP,
),
'install_finished' => array(
'display_name' => st('Finished'),
),
......@@ -1854,6 +1860,22 @@ function install_import_translations_remaining(&$install_state) {
}
}
/**
* Creates configuration translations.
*
* @param array $install_state
* An array of information about the current installation state.
*
* @return array
* The batch definition, if there are configuration objects to update.
*
* @see install_tasks()
*/
function install_update_configuration_translations(&$install_state) {
Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
return locale_config_batch_update_components(array(), array($install_state['parameters']['langcode']));
}
/**
* Performs final installation steps and displays a 'finished' page.
*
......
......@@ -14,7 +14,7 @@
*/
class ConfigLocaleOverrideWebTest extends WebTestBase {
public static $modules = array('locale', 'language', 'system', 'config_test');
public static $modules = array('locale', 'language', 'system');
public static function getInfo() {
return array(
......@@ -35,26 +35,35 @@ function testSiteNameTranslation() {
$adminUser = $this->drupalCreateUser(array('administer site configuration', 'administer languages'));
$this->drupalLogin($adminUser);
// Add French and make it the site default language.
$this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'fr'), t('Add language'));
// Add a custom lanugage.
$langcode = 'xx';
$name = $this->randomName(16);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'name' => $name,
'direction' => '0',
);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
// Save an override for the XX language.
config('locale.config.xx.system.site')->set('name', 'XX site name')->save();
$this->drupalLogout();
// The home page in English should not have the override.
$this->drupalGet('');
$this->assertNoText('French site name');
$this->assertNoText('XX site name');
// During path resolution the system.site configuration object is used to
// determine the front page. This occurs before language negotiation causing
// the configuration factory to cache an object without the correct
// overrides. The config_test module includes a
// locale.config.fr.system.site.yml which overrides the site name to 'French
// site name' to test that the configuration factory is re-initialised
// language negotiation. Ensure that it applies when we access the French
// front page.
// overrides. We are testing that the configuration factory is
// re-initialised after language negotiation. Ensure that it applies when
// we access the XX front page.
// @see \Drupal\Core\PathProcessor::processInbound()
$this->drupalGet('fr');
$this->assertText('French site name');
$this->drupalGet('xx');
$this->assertText('XX site name');
}
}
<?php
/**
* @file
* Contains \Drupal\locale\Locale.
*/
namespace Drupal\locale;
use Drupal;
/**
* Static service container wrapper for locale.
*/
class Locale {
/**
* Returns the locale configuration 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
*/
public static function config() {
return Drupal::service('locale.config.typed');
}
}
<?php
/**
* @file
* Contains \Drupal\locale\LocaleConfigManager.
*/
namespace Drupal\locale;
use Drupal\Core\Language\Language;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Config\StorageInterface;
/**
* Manages localized configuration type plugins.
*/
class LocaleConfigManager extends TypedConfigManager {
/**
* A storage controller instance for reading default configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $installStorage;
/**
* A string storage for reading and writing translations.
*/
protected $localeStorage;
/**
* Array with preloaded string translations.
*
* @var array
*/
protected $translations;
/**
* Creates a new typed configuration manager.
*
* @param \Drupal\Core\Config\StorageInterface $configStorage
* The storage controller object to use for reading configuration data.
* @param \Drupal\Core\Config\StorageInterface $schemaStorage
* The storage controller object to use for reading schema data.
* @param \Drupal\Core\Config\StorageInterface $installStorage
* The storage controller object to use for reading default configuration
* data.
* @param \Drupal\locale\StringStorageInterface $localeStorage
* (optional) The locale storage to use for reading string translations.
* Defaults to locale_storage().
*/
public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, StorageInterface $installStorage, StringStorageInterface $localeStorage = NULL) {
// Note we use the install storage for the parent constructor.
parent::__construct($configStorage, $schemaStorage);
$this->installStorage = $installStorage;
$this->localeStorage = $localeStorage ?: locale_storage();
}
/**
* Gets locale wrapper with typed configuration data.
*
* @param string $name
* Configuration object name.
*
* @return \Drupal\locale\LocaleTypedConfig
* Locale-wrapped configuration element.
*/
public function get($name) {
// Read default and current configuration data.
$default = $this->installStorage->read($name);
$updated = $this->configStorage->read($name);
// We get only the data that didn't change from default.
$data = $this->compareConfigData($default, $updated);
$definition = $this->getDefinition($name);
// Unless the configuration has a explicit language code we assume English.
$langcode = isset($default['langcode']) ? $default['langcode'] : 'en';
$wrapper = new LocaleTypedConfig($definition, $name, $langcode, $this);
$wrapper->setValue($data);
return $wrapper;
}
/**
* Compares default configuration with updated data.
*
* @param array $default
* Default configuration data.
* @param array|false $updated
* Current configuration data, or FALSE if no configuration data existed.
*
* @return array
* The elements of default configuration that haven't changed.
*/
protected function compareConfigData(array $default, $updated) {
// Speed up comparison, specially for install operations.
if ($default === $updated) {
return $default;
}
$result = array();
foreach ($default as $key => $value) {
if (isset($updated[$key])) {
if (is_array($value)) {
$result[$key] = $this->compareConfigData($value, $updated[$key]);
}
elseif ($value === $updated[$key]) {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Saves translated configuration data.
*
* @param string $name
* Configuration object name.
* @param string $langcode
* Language code.
* @param array $data
* Configuration data to be saved, that will be only the translated values.
*/
public function saveTranslationData($name, $langcode, array $data) {
$locale_name = self::localeConfigName($langcode, $name);
$this->configStorage->write($locale_name, $data);
}
/**
* Deletes translated configuration data.
*
* @param string $name
* Configuration object name.
* @param string $langcode
* Language code.
*/
public function deleteTranslationData($name, $langcode) {
$locale_name = self::localeConfigName($langcode, $name);
$this->configStorage->delete($locale_name);
}
/**
* Gets configuration names associated with components.
*
* @param array $components
* (optional) Array of component lists indexed by type. If not present or it
* is an empty array, it will update all components.
*
* @return array
* Array of configuration object names.
*/
public function getComponentNames(array $components) {
$components = array_filter($components);
if ($components) {
$names = array();
foreach ($components as $type => $list) {
// InstallStorage::getComponentNames returns a list of folders keyed by
// config name.
$names = array_merge($names, array_keys($this->installStorage->getComponentNames($type, $list)));
}
return $names;
}
else {
return $this->installStorage->listAll();
}
}
/**
* Deletes configuration translations for uninstalled components.
*
* @param array $components
* Array with string identifiers.
* @param array $langcodes
* Array of language codes.
*/
public function deleteComponentTranslations(array $components, array $langcodes) {
$names = $this->getComponentNames($components);
if ($names && $langcodes) {
foreach ($names as $name) {
foreach ($langcodes as $langcode) {
$this->deleteTranslationData($name, $langcode);
}
}
}
}
/**
* Gets configuration names associated with strings.
*
* @param array $lids
* Array with string identifiers.
*
* @return array
* Array of configuration object names.
*/
public function getStringNames(array $lids) {
$names = array();
$locations = $this->localeStorage->getLocations(array('sid' => $lids, 'type' => 'configuration'));
foreach ($locations as $location) {
$names[$location->name] = $location->name;
}
return $names;
}
/**
* Deletes configuration for language.
*
* @param string $langcode
* Language code to delete.
*/
public function deleteLanguageTranslations($langcode) {
$locale_name = self::localeConfigName($langcode);
foreach ($this->configStorage->listAll($locale_name) as $name) {
$this->configStorage->delete($name);
}
}
/**
* Translates string using the localization system.
*
* So far we only know how to translate strings from English so the source
* string should be in English.
* Unlike regular t() translations, strings will be added to the source
* tables only if this is marked as default data.
*
* @param string $name
* Name of the configuration location.
* @param string $langcode
* Language code to translate to.
* @param string $source
* The source string, should be English.
* @param string $context
* The string context.
*
* @return string|false
* Translated string if there is a translation, FALSE if not.
*/
public function translateString($name, $langcode, $source, $context) {
if ($source) {
// If translations for a language have not been loaded yet.
if (!isset($this->translations[$name][$langcode])) {
// Preload all translations for this configuration name and language.
$this->translations[$name][$langcode] = array();
foreach ($this->localeStorage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => $name)) as $string){
$this->translations[$name][$langcode][$string->context][$string->source] = $string;
}
}
if (!isset($this->translations[$name][$langcode][$context][$source])) {
// There is no translation of the source string in this config location
// to this language for this context.
if ($translation = $this->localeStorage->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode))) {
// Look for a translation of the string. It might have one, but not
// be saved in this configuration location yet.
// If the string has a translation for this context to this language,
// save it in the configuration location so it can be looked up faster
// next time.
$string = $this->localeStorage->createString((array) $translation)
->addLocation('configuration', $name)
->save();
}
else {
// No translation was found. Add the source to the configuration
// location so it can be translated, and the string is faster to look
// for next time.
$translation = $this->localeStorage
->createString(array('source' => $source, 'context' => $context))
->addLocation('configuration', $name)
->save();
}
// Add an entry, either the translation found, or a blank string object
// to track the source string, to this configuration location, language,
// and context.
$this->translations[$name][$langcode][$context][$source] = $translation;
}
// Return the string only when the string object had a translation.
if ($this->translations[$name][$langcode][$context][$source]->isTranslation()) {
return $this->translations[$name][$langcode][$context][$source]->getString();
}
}
return FALSE;
}
/**
* Provides configuration data location for given langcode and name.
*
* @param string $langcode
* The language code.
* @param string|NULL $name
* Name of the original configuration. Set to NULL to get the name prefix
* for all $langcode overrides.
*
* @return string
*/
public static function localeConfigName($langcode, $name = NULL) {
return rtrim('locale.config.' . $langcode . '.' . $name, '.');
}
}
<?php
/**
* @file
* Contains \Drupal\locale\LocaleTypedConfig.
*/
namespace Drupal\locale;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\Config\Schema\Element;
use Drupal\Core\Config\Schema\ArrayElement;
/**
* Defines the locale configuration wrapper object.
*/
class LocaleTypedConfig extends Element {
/**
* The typed configuration data.
*
* @var \Drupal\Core\Config\Schema\Element
*/
protected $typedConfig;
/**
* The language code for which this is a translation.
*
* @var string
*/
protected $langcode;
/**
* The locale configuration manager object.
*
* @var \Drupal\locale\LocaleConfigManager
*/
protected $localeConfig;
/**
* Constructs a configuration wrapper object.
*
* @param array $definition
* The data definition.
* @param string $name
* The configuration object name.
* @param string $langcode
* Language code for the source configuration data.
* @param \Drupal\locale\LocaleConfigManager $localeConfig;
* The locale configuration manager object.
*/
public function __construct(array $definition, $name, $langcode, \Drupal\locale\LocaleConfigManager $localeConfig) {
parent::__construct($definition, $name);
$this->langcode = $langcode;
$this->localeConfig = $localeConfig;
}
/**
* Gets wrapped typed config object.
*/
public function getTypedConfig() {
return $this->localeConfig->create($this->definition, $this->value);
}
/**
* {@inheritdoc}
*/
public function getTranslation($langcode) {
$options = array(
'source' => $this->langcode,
'target' => $langcode,
);
$data = $this->getElementTranslation($this->getTypedConfig(), $options);
return $this->localeConfig->create($this->definition, $data);
}
/**
* {@inheritdoc}
*/
public function language() {
return language_load($this->langcode);
}
/**
* Checks whether we can translate these languages.
*
* @param string $from_langcode
* Source language code.
* @param string $to_langcode
* Destination language code.
*
* @return bool
* TRUE if this translator supports translations for these languages.
*/
protected function canTranslate($from_langcode, $to_langcode) {
if ($from_langcode == 'en') {
return TRUE;
}
return FALSE;
}
/**
* Gets translated configuration data for a typed configuration element.
*
* @param mixed $element
* Typed configuration element, either \Drupal\Core\Config\Schema\Element or
* \Drupal\Core\Config\Schema\ArrayElement.
* @param array $options
* Array with translation options that must contain the keys defined in
* \Drupal\locale\LocaleTypedConfig::translateElement()
*
* @return array
* Configuration data translated to the requested language if available,
* an empty array otherwise.
*/
protected function getElementTranslation($element, array $options) {
$translation = array();
if ($element instanceof ArrayElement) {
$translation = $this->getArrayTranslation($element, $options);
}
elseif ($this->translateElement($element, $options)) {
$translation = $element->getValue();
}
return $translation;
}
/**
* Gets translated configuration data for an element of type ArrayElement.
*
* @param \Drupal\Core\Config\Schema\ArrayElement $element
* Typed configuration array element.
* @param array $options
* Array with translation options that must contain the keys defined in
* \Drupal\locale\LocaleTypedConfig::translateElement()
*
* @return array
* Configuration data translated to the requested language.
*/
protected function getArrayTranslation(ArrayElement $element, array $options) {
$translation = array();
foreach ($element as $key => $property) {
$value = $this->getElementTranslation($property, $options);
if (!empty($value)) {
$translation[$key] = $value;
}
}
return $translation;
}
/**
* Translates element's value if it fits our translation criteria.
*
* For an element to be translatable by locale module it needs to be of base
* type 'string' and have 'translatable = TRUE' in the element's definition.
* Translatable elements may use these additional keys in their data
* definition:
* - 'translatable', FALSE to opt out of translation.
* - 'locale context', to define the string context.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $element
* Configuration element.
* @param array $options
* Array with translation options that must contain the following keys:
* - 'source', Source language code.
* - 'target', Target language code.
*
* @return bool
* Whether the element fits the translation criteria.
*/
protected function translateElement(\Drupal\Core\TypedData\TypedDataInterface $element, array $options) {
if ($this->canTranslate($options['source'], $options['target'])) {
$definition = $element->getDefinition();
$value = $element->getValue();
if ($value && !empty($definition['translatable'])) {
$context = isset($definition['locale context']) ? $definition['locale context'] : '';
if ($translation = $this->localeConfig->translateString($this->name, $options['target'], $value, $context)) {
$element->setValue($translation);
return TRUE;
}
}
}
// The element does not have a translation.
return FALSE;
}
}