Commit 0a7f5b84 authored by catch's avatar catch

Issue #2244447 by alexpott, sun, Berdir: Translation of low-level...

Issue #2244447 by alexpott, sun, Berdir: Translation of low-level info/annotations leads to circular dependencies.
parent d084d2d2
...@@ -983,7 +983,7 @@ function form_select_options($element, $choices = NULL) { ...@@ -983,7 +983,7 @@ function form_select_options($element, $choices = NULL) {
$options .= form_select_options($element, $choice); $options .= form_select_options($element, $choice);
$options .= '</optgroup>'; $options .= '</optgroup>';
} }
elseif (is_object($choice)) { elseif (is_object($choice) && isset($choice->option)) {
$options .= form_select_options($element, $choice->option); $options .= form_select_options($element, $choice->option);
} }
else { else {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
namespace Drupal\Core\Annotation; namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\AnnotationBase; use Drupal\Component\Annotation\AnnotationBase;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationWrapper;
/** /**
* @defgroup plugin_translatable Translatable plugin metadata * @defgroup plugin_translatable Translatable plugin metadata
...@@ -52,12 +52,11 @@ ...@@ -52,12 +52,11 @@
* @ingroup plugin_translatable * @ingroup plugin_translatable
*/ */
class Translation extends AnnotationBase { class Translation extends AnnotationBase {
use StringTranslationTrait;
/** /**
* The translation of the value passed to the constructor of the class. * The string translation object.
* *
* @var string * @var \Drupal\Core\StringTranslation\TranslationWrapper
*/ */
protected $translation; protected $translation;
...@@ -83,11 +82,11 @@ public function __construct(array $values) { ...@@ -83,11 +82,11 @@ public function __construct(array $values) {
'context' => $values['context'], 'context' => $values['context'],
); );
} }
$this->translation = $this->t($string, $arguments, $options); $this->translation = new TranslationWrapper($string, $arguments, $options);
} }
/** /**
* Implements Drupal\Core\Annotation\AnnotationInterface::get(). * {@inheritdoc}
*/ */
public function get() { public function get() {
return $this->translation; return $this->translation;
......
...@@ -183,7 +183,7 @@ protected function castValue($key, $value) { ...@@ -183,7 +183,7 @@ protected function castValue($key, $value) {
if ($element && ($element instanceof Undefined || $element instanceof Ignore)) { if ($element && ($element instanceof Undefined || $element instanceof Ignore)) {
return $value; return $value;
} }
if ((is_scalar($value) || $value === NULL)) { if (is_scalar($value) || $value === NULL) {
if ($element && $element instanceof PrimitiveInterface) { if ($element && $element instanceof PrimitiveInterface) {
// Special handling for integers and floats since the configuration // Special handling for integers and floats since the configuration
// system is primarily concerned with saving values from the Form API // system is primarily concerned with saving values from the Form API
......
...@@ -43,7 +43,7 @@ public static function flattenOptions(array $array) { ...@@ -43,7 +43,7 @@ public static function flattenOptions(array $array) {
*/ */
protected static function doFlattenOptions(array $array, array &$options) { protected static function doFlattenOptions(array $array, array &$options) {
foreach ($array as $key => $value) { foreach ($array as $key => $value) {
if (is_object($value)) { if (is_object($value) && isset($value->option)) {
static::doFlattenOptions($value->option, $options); static::doFlattenOptions($value->option, $options);
} }
elseif (is_array($value)) { elseif (is_array($value)) {
......
<?php
/**
* @file
* Contains \Drupal\Core\StringTranslation\TranslationWrapper.
*/
namespace Drupal\Core\StringTranslation;
/**
* Provides a class to wrap a translatable string.
*
* This class can be used to delay translating strings until the translation
* system is ready. This is useful for using translation in very low level
* subsystems like entity definition and stream wrappers.
*
* @see \Drupal\Core\Annotation\Translation
*/
class TranslationWrapper {
use StringTranslationTrait;
/**
* The string to be translated.
*
* @var string
*/
protected $string;
/**
* The translation arguments.
*
* @var array
*/
protected $arguments;
/**
* The translation options.
*
* @var array
*/
protected $options;
/**
* Constructs a new class instance.
*
* Parses values passed into this class through the t() function in Drupal and
* handles an optional context for the string.
*
* @param string $string
* The string that is to be translated.
* @param array $arguments
* (optional) An array with placeholder replacements, keyed by placeholder.
* @param array $options
* (optional) An array of additional options.
*/
public function __construct($string, array $arguments = array(), array $options = array()) {
$this->string = $string;
$this->arguments = $arguments;
$this->options = $options;
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
return $this->render();
}
/**
* Renders the object as a string.
*
* @return string
* The translated string.
*/
public function render() {
return $this->t($this->string, $this->arguments, $this->options);
}
/**
* Magic __sleep() method to avoid serializing the string translator.
*/
public function __sleep() {
return array('string', 'arguments', 'options');
}
}
...@@ -36,7 +36,9 @@ public function label() { ...@@ -36,7 +36,9 @@ public function label() {
} }
$definition = $this->getPluginDefinition(); $definition = $this->getPluginDefinition();
return $definition['admin_label']; // Cast the admin label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
return (string) $definition['admin_label'];
} }
/** /**
......
...@@ -82,7 +82,7 @@ public function testManager() { ...@@ -82,7 +82,7 @@ public function testManager() {
$this->editorManager->clearCachedDefinitions(); $this->editorManager->clearCachedDefinitions();
// Case 2: a text editor available. // Case 2: a text editor available.
$this->assertIdentical(array('unicorn' => 'Unicorn Editor'), $this->editorManager->listOptions(), 'When some text editor is enabled, the manager works correctly.'); $this->assertIdentical('Unicorn Editor', (string) $this->editorManager->listOptions()['unicorn'], 'When some text editor is enabled, the manager works correctly.');
// Case 3: a text editor available & associated (but associated only with // Case 3: a text editor available & associated (but associated only with
// the 'Full HTML' text format). // the 'Full HTML' text format).
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\language\Entity\Language as LanguageEntity; use Drupal\language\Entity\Language as LanguageEntity;
use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Crypt;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
...@@ -209,9 +210,9 @@ function locale_theme() { ...@@ -209,9 +210,9 @@ function locale_theme() {
function locale_stream_wrappers() { function locale_stream_wrappers() {
$wrappers = array( $wrappers = array(
'translations' => array( 'translations' => array(
'name' => t('Translation files'), 'name' => new TranslationWrapper('Translation files'),
'class' => 'Drupal\locale\TranslationsStream', 'class' => 'Drupal\locale\TranslationsStream',
'description' => t('Translation files'), 'description' => new TranslationWrapper('Translation files'),
'type' => STREAM_WRAPPERS_LOCAL_HIDDEN, 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
), ),
); );
......
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleLocaleLookupTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
class LocaleLocaleLookupTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Test LocaleLookup',
'description' => 'Tests LocaleLookup does not cause circular references.',
'group' => 'Locale',
);
}
/**
* Tests hasTranslation().
*/
public function testCircularDependency() {
// Change the language default object to different values.
$new_language_default = new Language(array(
'id' => 'fr',
'name' => 'French',
'direction' => 0,
'weight' => 0,
'method_id' => 'language-default',
'default' => TRUE,
));
language_save($new_language_default);
$this->drupalLogin($this->root_user);
// Ensure that we can enable early_translation_test on a non-english site.
$this->drupalPostForm('admin/modules', array('modules[Testing][early_translation_test][enable]' => TRUE), t('Save configuration'));
$this->assertResponse(200);
}
}
name: 'Early translation test'
type: module
description: 'Support module for testing early bootstrap getting of annotations with translations.'
core: 8.x
package: Testing
version: VERSION
services:
authentication.early_translation_test:
class: Drupal\early_translation_test\Auth
arguments: ['@entity.manager']
tags:
- { name: authentication_provider, priority: 100 }
<?php
/**
* @file
* Contains \Drupal\early_translation_test\Auth.
*/
namespace Drupal\early_translation_test;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Test authentication provider.
*/
class Auth implements AuthenticationProviderInterface {
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs an authentication provider object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(EntityManagerInterface $entity_manager) {
// Authentication providers are called early during in the bootstrap.
// Getting the user storage used to result in a circular reference since
// translation involves a call to \Drupal\locale\LocaleLookup that tries to
// get the user roles.
// @see https://drupal.org/node/2241461
$this->userStorage = $entity_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function cleanup(Request $request) {}
/**
* {@inheritdoc}
*/
public function handleException(GetResponseForExceptionEvent $event) {
return FALSE;
}
}
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Plugin\Discovery; namespace Drupal\system\Tests\Plugin\Discovery;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\simpletest\UnitTestBase; use Drupal\simpletest\UnitTestBase;
/** /**
...@@ -49,7 +50,7 @@ function testDiscoveryInterface() { ...@@ -49,7 +50,7 @@ function testDiscoveryInterface() {
// Ensure that getDefinition() returns the expected definition. // Ensure that getDefinition() returns the expected definition.
foreach ($this->expectedDefinitions as $id => $definition) { foreach ($this->expectedDefinitions as $id => $definition) {
$this->assertIdentical($this->discovery->getDefinition($id), $definition); $this->assertDefinitionIdentical($this->discovery->getDefinition($id), $definition);
} }
// Ensure that an empty array is returned if no plugin definitions are found. // Ensure that an empty array is returned if no plugin definitions are found.
...@@ -58,5 +59,30 @@ function testDiscoveryInterface() { ...@@ -58,5 +59,30 @@ function testDiscoveryInterface() {
// Ensure that NULL is returned as the definition of a non-existing plugin. // Ensure that NULL is returned as the definition of a non-existing plugin.
$this->assertIdentical($this->emptyDiscovery->getDefinition('non_existing', FALSE), NULL, 'NULL returned as the definition of a non-existing plugin.'); $this->assertIdentical($this->emptyDiscovery->getDefinition('non_existing', FALSE), NULL, 'NULL returned as the definition of a non-existing plugin.');
} }
/**
* Asserts a definition against an expected definition.
*
* Converts any instances of \Drupal\Core\Annotation\Translation to a string.
*
* @param array $definition
* The definition to test.
* @param array $expected_definition
* The expected definition to test against.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertDefinitionIdentical(array $definition, array $expected_definition) {
$func = function (&$item){
if ($item instanceof TranslationWrapper) {
$item = (string) $item;
}
};
array_walk_recursive($definition, $func);
array_walk_recursive($expected_definition, $func);
return $this->assertIdentical($definition, $expected_definition);
}
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\block\BlockPluginInterface; use Drupal\block\BlockPluginInterface;
use Drupal\user\UserInterface; use Drupal\user\UserInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
...@@ -719,15 +720,15 @@ function system_theme_suggestions_field(array $variables) { ...@@ -719,15 +720,15 @@ function system_theme_suggestions_field(array $variables) {
function system_stream_wrappers() { function system_stream_wrappers() {
$wrappers = array( $wrappers = array(
'public' => array( 'public' => array(
'name' => t('Public files'), 'name' => new TranslationWrapper('Public files'),
'class' => 'Drupal\Core\StreamWrapper\PublicStream', 'class' => 'Drupal\Core\StreamWrapper\PublicStream',
'description' => t('Public local files served by the webserver.'), 'description' => new TranslationWrapper('Public local files served by the webserver.'),
'type' => STREAM_WRAPPERS_LOCAL_NORMAL, 'type' => STREAM_WRAPPERS_LOCAL_NORMAL,
), ),
'temporary' => array( 'temporary' => array(
'name' => t('Temporary files'), 'name' => new TranslationWrapper('Temporary files'),
'class' => 'Drupal\Core\StreamWrapper\TemporaryStream', 'class' => 'Drupal\Core\StreamWrapper\TemporaryStream',
'description' => t('Temporary local files for upload and previews.'), 'description' => new TranslationWrapper('Temporary local files for upload and previews.'),
'type' => STREAM_WRAPPERS_LOCAL_HIDDEN, 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
), ),
); );
...@@ -735,9 +736,9 @@ function system_stream_wrappers() { ...@@ -735,9 +736,9 @@ function system_stream_wrappers() {
// Only register the private file stream wrapper if a file path has been set. // Only register the private file stream wrapper if a file path has been set.
if (\Drupal::config('system.file')->get('path.private')) { if (\Drupal::config('system.file')->get('path.private')) {
$wrappers['private'] = array( $wrappers['private'] = array(
'name' => t('Private files'), 'name' => new TranslationWrapper('Private files'),
'class' => 'Drupal\Core\StreamWrapper\PrivateStream', 'class' => 'Drupal\Core\StreamWrapper\PrivateStream',
'description' => t('Private local files served by Drupal.'), 'description' => new TranslationWrapper('Private local files served by Drupal.'),
'type' => STREAM_WRAPPERS_LOCAL_NORMAL, 'type' => STREAM_WRAPPERS_LOCAL_NORMAL,
); );
} }
......
...@@ -195,7 +195,9 @@ public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) { ...@@ -195,7 +195,9 @@ public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
$display_options = array( $display_options = array(
'display_plugin' => $plugin_id, 'display_plugin' => $plugin_id,
'id' => $id, 'id' => $id,
'display_title' => $title, // Cast the display title to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
'display_title' => (string) $title,
'position' => $id === 'default' ? 0 : count($this->display), 'position' => $id === 'default' ? 0 : count($this->display),
'provider' => $plugin['provider'], 'provider' => $plugin['provider'],
'display_options' => array(), 'display_options' => array(),
......
...@@ -1160,11 +1160,13 @@ protected function prepareFilterSelectOptions(&$options) { ...@@ -1160,11 +1160,13 @@ protected function prepareFilterSelectOptions(&$options) {
} }
// FAPI has some special value to allow hierarchy. // FAPI has some special value to allow hierarchy.
// @see _form_options_flatten // @see _form_options_flatten
elseif (is_object($label)) { elseif (is_object($label) && isset($label->option)) {
$this->prepareFilterSelectOptions($options[$value]->option); $this->prepareFilterSelectOptions($options[$value]->option);
} }
else { else {
$options[$value] = strip_tags(decode_entities($label)); // Cast the label to a string since it can be an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
$options[$value] = strip_tags(decode_entities((string) $label));
} }
} }
} }
......
...@@ -79,7 +79,9 @@ protected function defineOptions() { ...@@ -79,7 +79,9 @@ protected function defineOptions() {
// Relationships definitions should define a default label, but if they aren't get another default value. // Relationships definitions should define a default label, but if they aren't get another default value.
if (!empty($this->definition['label'])) { if (!empty($this->definition['label'])) {
$label = $this->definition['label']; // Cast the label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
$label = (string) $this->definition['label'];
} }
else { else {
$label = !empty($this->definition['field']) ? $this->definition['field'] : $this->definition['base field']; $label = !empty($this->definition['field']) ? $this->definition['field'] : $this->definition['base field'];
......
...@@ -229,13 +229,15 @@ protected function getDisplaysList(EntityInterface $view) { ...@@ -229,13 +229,15 @@ protected function getDisplaysList(EntityInterface $view) {
$displays = array(); $displays = array();
foreach ($view->get('display') as $display) { foreach ($view->get('display') as $display) {
$definition = $this->displayManager->getDefinition($display['display_plugin']); $definition = $this->displayManager->getDefinition($display['display_plugin']);
if (!empty($definition['admin'])) { if (isset($definition['admin'])) {
$displays[$definition['admin']] = TRUE; // Cast the admin label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
$displays[] = (string) $definition['admin'];
} }
} }
ksort($displays); sort($displays);
return array_keys($displays); return $displays;
} }
/** /**
......
...@@ -62,7 +62,7 @@ public function testGet(array $values, $expected) { ...@@ -62,7 +62,7 @@ public function testGet(array $values, $expected) {
$annotation = new Translation($values); $annotation = new Translation($values);
$this->assertSame($expected, $annotation->get()); $this->assertSame($expected, (string) $annotation->get());
} }
/** /**
......
...@@ -50,11 +50,15 @@ public function providerTestFlattenOptions() { ...@@ -50,11 +50,15 @@ public function providerTestFlattenOptions() {
$object1->option = array('foo' => 'foo'); $object1->option = array('foo' => 'foo');
$object2 = new \stdClass(); $object2 = new \stdClass();
$object2->option = array(array('foo' => 'foo'), array('foo' => 'foo')); $object2->option = array(array('foo' => 'foo'), array('foo' => 'foo'));
$object3 = new \stdClass();
return array( return array(
array(array('foo' => 'foo')), array(array('foo' => 'foo')),
array(array(array('foo' => 'foo'))), array(array(array('foo' => 'foo'))),
array(array($object1)), array(array($object1)),
array(array($object2)), array(array($object2)),
array(array($object1, $object2)),
array(array('foo' => $object3)),
array(array('foo' => $object3, $object1, array('foo' => 'foo'))),
); );
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment