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');
}
}
This diff is collapsed.
<?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;
}
}
......@@ -102,6 +102,7 @@ function setReport($report = array()) {
'updates' => 0,
'deletes' => 0,
'skips' => 0,
'strings' => array(),
);
$this->_report = $report;
}
......@@ -259,6 +260,7 @@ private function importString(PoItem $item) {
$string->save();
$this->_report['updates']++;
}
$this->_report['strings'][] = $string->getId();
return $string->lid;
}
else {
......@@ -273,6 +275,7 @@ private function importString(PoItem $item) {
))->save();
$this->_report['additions']++;
$this->_report['strings'][] = $string->getId();
return $string->lid;
}
}
......@@ -280,6 +283,7 @@ private function importString(PoItem $item) {
// Empty translation, remove existing if instructed.
$string->delete();
$this->_report['deletes']++;
$this->_report['strings'][] = $string->lid;
return $string->lid;
}
}
......
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleConfigTranslationTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
use Drupal\locale\LocaleTypedConfig;
/**
* Tests Metadata for configuration objects.
*/
class LocaleConfigTranslationTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
public static function getInfo() {
return array(
'name' => 'Configuration translation',
'description' => 'Tests translation of configuration strings.',
'group' => 'Locale',
);
}
public function setUp() {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = locale_storage();
}
/**
* Tests basic configuration translation.
*/
function testConfigTranslation() {
// Add custom language.
$langcode = 'xx';
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface', 'administer modules'));
$this->drupalLogin($admin_user);
$name = $this->randomName(16);
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'name' => $name,
'direction' => '0',
);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
$language = new Language(array('langcode' => $langcode));
// Set path prefix.
$edit = array( "prefix[$langcode]" => $langcode );
$this->drupalPost('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
// Check site name string exists and create translation for it.
$string = $this->storage->findString(array('source' => 'Drupal', 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
// Translate using the UI so configuration is refreshed.
$site_name = $this->randomName(20);
$search = array(
'string' => $string->source,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$textareas = $this->xpath('//textarea');
$textarea = current($textareas);
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $site_name,
);
$this->drupalPost('admin/config/regional/translate/translate', $edit, t('Save translations'));
$wrapper = $this->container->get('locale.config.typed')->get('system.site');
// Get translation and check we've only got the site name.
$translation = $wrapper->getTranslation($langcode);
$properties = $translation->getProperties();
$this->assertEqual(count($properties), 1, 'Got the right number of properties after translation');
$this->assertEqual($properties['name']->getValue(), $site_name, 'Got the right translation for site name after translation');
// Check the translated site name is displayed.
$this->drupalGet($langcode);
$this->assertText($site_name, 'The translated site name is displayed after translations refreshed.');
// Assert strings from image module config are not available.
$string = $this->storage->findString(array('source' => 'Medium (220x220)', 'context' => '', 'type' => 'configuration'));
$this->assertFalse($string, 'Configuration strings have been created upon installation.');
// Enable the image module.
$this->drupalPost('admin/modules', array('modules[Core][image][enable]' => "1"), t('Save configuration'));
$this->resetAll();
$string = $this->storage->findString(array('source' => 'Medium (220x220)', 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
$locations = $string->getLocations();
$this->assertTrue(isset($locations['configuration']) && isset($locations['configuration']['image.style.medium']), 'Configuration string has been created with the right location');
// Check the string is unique and has no translation yet.
$translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium'));
$translation = reset($translations);
$this->assertTrue(count($translations) == 1 && $translation->source == $string->source && empty($translation->translation), 'Got only one string for image configuration and has no translation.');
// Translate using the UI so configuration is refreshed.
$image_style_label = $this->randomName(20);
$search = array(
'string' => $string->source,
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea[0]['name'];
$edit = array(
$lid => $image_style_label,
);
$this->drupalPost('admin/config/regional/translate/translate', $edit, t('Save translations'));
// Check the right single translation has been created.
$translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium'));
$translation = reset($translations);
$this->assertTrue(count($translations) == 1 && $translation->source == $string->source && $translation->translation == $image_style_label, 'Got only one translation for image configuration.');
// Try more complex configuration data.
$wrapper = $this->container->get('locale.config.typed')->get('image.style.medium');
$translation = $wrapper->getTranslation($langcode);
$property = $translation->get('label');
$this->assertEqual($property->getValue(), $image_style_label, 'Got the right translation for image style name after translation');
// Quick test to ensure translation file exists.
$this->assertEqual(config('locale.config.xx.image.style.medium')->get('label'), $image_style_label);
// Disable and uninstall the module.
$this->drupalPost('admin/modules', array('modules[Core][image][enable]' => FALSE), t('Save configuration'));
$this->drupalPost('admin/modules/uninstall', array('uninstall[image]' => "image"), t('Uninstall'));
$this->drupalPost(NULL, array(), t('Uninstall'));
// Ensure that the translated configuration has been removed.
$this->assertFalse(config('locale.config.xx.image.style.medium')->get('label'), 'Translated configuration for image module removed.');
}
}
......@@ -237,6 +237,71 @@ function testEmptyMsgstr() {
$this->assertText($str, t('Search found the string as untranslated.'));
}
/**
* Tests .po file import with configuration translation.
*/
function testConfigPoFile() {
// Values for translations to assert. Config key, original string,
// translation and config property name.
$config_strings = array(
'system.maintenance' => array(
'@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
'@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
'message',
),
'user.role.anonymous' => array(
'Anonymous user',
'Névtelen felhasználó',
'label',
),
);
// Add custom language for testing.
$langcode = 'xx';
$edit = array(
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'name' => $this->randomName(16),
'direction' => '0',
);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
// Check for the source strings we are going to translate. Adding the
// custom language should have made the process to export configuration
// strings to interface translation executed.
$locale_storage = locale_storage();
foreach ($config_strings as $config_string) {
$string = $locale_storage->findString(array('source' => $config_string[0], 'context' => '', 'type' => 'configuration'));
$this->assertTrue($string, 'Configuration strings have been created upon installation.');
}
// Import a .po file to translate.
$this->importPoFile($this->getPoFileWithConfig(), array(
'langcode' => $langcode,
));
// Translations got recorded in the interface translation system.
foreach ($config_strings as $config_string) {
$search = array(
'string' => $config_string[0],
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertText($config_string[1], format_string('Translation of @string found.', array('@string' => $config_string[0])));
}
$locale_config = $this->container->get('locale.config.typed');
// Translations got recorded in the config system.
foreach ($config_strings as $config_key => $config_string) {
$wrapper = $locale_config->get($config_key);
$translation = $wrapper->getTranslation($langcode);
$properties = $translation->getProperties();
$this->assertEqual(count($properties), 1, 'Got the right number of properties with strict translation');
$this->assertEqual($properties[$config_string[2]]->getValue(), $config_string[1]);
}
}
/**
* Helper function: import a standalone .po file in a given language.
*
......@@ -449,6 +514,7 @@ function getPoFileWithEmptyMsgstr() {
EOF;
}
/**
* Helper function that returns a .po file with an empty last item.
*/
......@@ -468,6 +534,28 @@ function getPoFileWithMsgstr() {
msgid "Will not appear in Drupal core, so we can ensure the test passes"
msgstr ""
EOF;
}
/**
* Helper function that returns a .po file with configuration translations.
*/
function getPoFileWithConfig() {
return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
msgid "Anonymous user"
msgstr "Névtelen felhasználó"
EOF;
}
......
This diff is collapsed.
......@@ -319,6 +319,9 @@ function locale_language_delete($language) {
module_load_include('inc', 'locale', 'locale.bulk');
locale_translate_delete_translation_files(array(), array($language->langcode));
// Remove translated configuration objects.
\Drupal\locale\Locale::config()->deleteLanguageTranslations($language->langcode);
// Changing the language settings impacts the interface:
_locale_invalidate_js($language->langcode);
cache('page')->deleteAll();
......@@ -473,14 +476,16 @@ function locale_get_plural($count, $langcode = NULL) {
* Implements hook_modules_installed().
*/
function locale_modules_installed($modules) {
locale_system_update($modules);
$components['module'] = $modules;
locale_system_update($components);
}
/**
* Implements hook_modules_uninstalled().
*/
function locale_modules_uninstalled($modules) {
locale_system_remove($modules);
$components['module'] = $modules;
locale_system_remove($components);
}
/**
......@@ -490,14 +495,16 @@ function locale_modules_uninstalled($modules) {
* initial installation. The theme system is missing an installation hook.
*/
function locale_themes_enabled($themes) {
locale_system_update($themes);
$components['theme'] = $themes;
locale_system_update($components);
}
/**
* Implements hook_themes_disabled().
*/
function locale_themes_disabled($themes) {
locale_system_remove($themes);
$components['theme'] = $themes;
locale_system_remove($components);
}
/**
......@@ -507,10 +514,14 @@ function locale_themes_disabled($themes) {
* components.
*
* @param array $components
* An array of component (theme and/or module) names to import
* translations for.
* An array of arrays of component (theme and/or module) names to import
* translations for, indexed by type.
*/
function locale_system_update($components) {
function locale_system_update(array $components) {
$components += array('module' => array(), 'theme' => array());
$list = array_merge($components['module'], $components['theme']);
// Skip running the translation imports if in the installer,
// because it would break out of the installer flow. We have
// built-in support for translation imports in the installer.
......@@ -521,11 +532,15 @@ function locale_system_update($components) {
// Only when new projects are added the update batch will be triggered. Not
// each enabled module will introduce a new project. E.g. sub modules.
$projects = array_keys(locale_translation_build_projects());
if ($components = array_intersect($components, $projects)) {
if ($list = array_intersect($list, $projects)) {