diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index b24430c37e3b9b8d2ad8d31e07a92d80dc37e8c2..0f6a6c6421fb4539cff35fa9ecc57dd2fa792672 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -13,6 +13,7 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Render\SafeString; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Utility\Error; @@ -490,9 +491,9 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) $_SESSION['messages'][$type] = array(); } - // Convert strings which are in the safe markup list to SafeString objects. - if (is_string($message) && SafeMarkup::isSafe($message)) { - $message = \Drupal\Core\Render\SafeString::create($message); + // Convert strings which are safe to the simplest SafeString objects. + if (!($message instanceof SafeString) && SafeMarkup::isSafe($message)) { + $message = SafeString::create((string) $message); } // Do not use strict type checking so that equivalent string and diff --git a/core/includes/common.inc b/core/includes/common.inc index 38a2a3ddbb46a364a81ab14ee80af2503796dc69..ece80d11b591e2a7f03029fd375297323defc897 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -25,6 +25,7 @@ use Drupal\Core\Render\SafeString; use Drupal\Core\Render\Renderer; use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; @@ -260,7 +261,7 @@ function check_url($uri) { * Optional language code to translate to a language other than what is used * to display the page. * - * @return + * @return \Drupal\Core\StringTranslation\TranslationWrapper * A translated string representation of the size. */ function format_size($size, $langcode = NULL) { @@ -269,16 +270,7 @@ function format_size($size, $langcode = NULL) { } else { $size = $size / Bytes::KILOBYTE; // Convert bytes to kilobytes. - $units = array( - t('@size KB', array(), array('langcode' => $langcode)), - t('@size MB', array(), array('langcode' => $langcode)), - t('@size GB', array(), array('langcode' => $langcode)), - t('@size TB', array(), array('langcode' => $langcode)), - t('@size PB', array(), array('langcode' => $langcode)), - t('@size EB', array(), array('langcode' => $langcode)), - t('@size ZB', array(), array('langcode' => $langcode)), - t('@size YB', array(), array('langcode' => $langcode)), - ); + $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; foreach ($units as $unit) { if (round($size, 2) >= Bytes::KILOBYTE) { $size = $size / Bytes::KILOBYTE; @@ -287,7 +279,26 @@ function format_size($size, $langcode = NULL) { break; } } - return str_replace('@size', round($size, 2), $unit); + $args = ['@size' => round($size, 2)]; + $options = ['langcode' => $langcode]; + switch ($unit) { + case 'KB': + return new TranslationWrapper('@size KB', $args, $options); + case 'MB': + return new TranslationWrapper('@size MB', $args, $options); + case 'GB': + return new TranslationWrapper('@size GB', $args, $options); + case 'TB': + return new TranslationWrapper('@size TB', $args, $options); + case 'PB': + return new TranslationWrapper('@size PB', $args, $options); + case 'EB': + return new TranslationWrapper('@size EB', $args, $options); + case 'ZB': + return new TranslationWrapper('@size ZB', $args, $options); + case 'YB': + return new TranslationWrapper('@size YB', $args, $options); + } } } diff --git a/core/lib/Drupal/Component/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php index c845c4b07ee431675d03befb06cb776907b9847d..f6737528578833b8765cce9d78344d86507e3b7a 100644 --- a/core/lib/Drupal/Component/Gettext/PoItem.php +++ b/core/lib/Drupal/Component/Gettext/PoItem.php @@ -193,7 +193,7 @@ public function setFromArray(array $values = array()) { strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) { $this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source)); $this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation)); - $this->setPlural(count($this->_translation) > 1); + $this->setPlural(count($this->_source) > 1); } } diff --git a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..0592c377d6128545ec8f8e8648dfa4c169b3817c --- /dev/null +++ b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\Utility\PlaceholderTrait. + */ + +namespace Drupal\Component\Utility; + +/** + * Offers functionality for formatting strings using placeholders. + */ +trait PlaceholderTrait { + + /** + * Formats a string by replacing variable placeholders. + * + * @param string $string + * A string containing placeholders. + * @param array $args + * An associative array of replacements to make. + * @param bool &$safe + * A boolean indicating whether the string is safe or not (optional). + * + * @return string + * The string with the placeholders replaced. + * + * @see \Drupal\Component\Utility\SafeMarkup::format() + * @see \Drupal\Core\StringTranslation\TranslationWrapper::render() + */ + protected static function placeholderFormat($string, array $args, &$safe = TRUE) { + // Transform arguments before inserting them. + foreach ($args as $key => $value) { + switch ($key[0]) { + case '@': + // Escaped only. + if (!SafeMarkup::isSafe($value)) { + $args[$key] = Html::escape($value); + } + break; + + case '%': + default: + // Escaped and placeholder. + if (!SafeMarkup::isSafe($value)) { + $value = Html::escape($value); + } + $args[$key] = '<em class="placeholder">' . $value . '</em>'; + break; + + case '!': + // Pass-through. + if (!SafeMarkup::isSafe($value)) { + $safe = FALSE; + } + } + } + return strtr($string, $args); + } + +} diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php index 3020bbe5711c3b7fcf85661570db20bf7db8e127..00b09f2574345c1f8d065c175e553d638d1b3214 100644 --- a/core/lib/Drupal/Component/Utility/SafeMarkup.php +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php @@ -31,6 +31,7 @@ * @see theme_render */ class SafeMarkup { + use PlaceholderTrait; /** * The list of safe strings. @@ -204,40 +205,12 @@ public static function checkPlain($text) { */ public static function format($string, array $args) { $safe = TRUE; - - // Transform arguments before inserting them. - foreach ($args as $key => $value) { - switch ($key[0]) { - case '@': - // Escaped only. - if (!SafeMarkup::isSafe($value)) { - $args[$key] = Html::escape($value); - } - break; - - case '%': - default: - // Escaped and placeholder. - if (!SafeMarkup::isSafe($value)) { - $value = Html::escape($value); - } - $args[$key] = '<em class="placeholder">' . $value . '</em>'; - break; - - case '!': - // Pass-through. - if (!static::isSafe($value)) { - $safe = FALSE; - } - } - } - - $output = strtr($string, $args); + $output = static::placeholderFormat($string, $args, $safe); if ($safe) { static::$safeStrings[$output]['html'] = TRUE; } - return $output; + } } diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index dd3c8f0e62eecea2c878bbd6a15ed694c3c869c1..9d749fbedd78b3a60ebc8bbe3d5bc6697563e8f9 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -945,9 +945,6 @@ public function getEntityTypeLabels($group = FALSE) { foreach ($definitions as $entity_type_id => $definition) { if ($group) { - // We cast the optgroup label to string as array keys must not be - // objects and t() may return a TranslationWrapper once issue #2557113 - // lands. $options[(string) $definition->getGroupLabel()][$entity_type_id] = $definition->getLabel(); } else { @@ -963,8 +960,6 @@ public function getEntityTypeLabels($group = FALSE) { // Make sure that the 'Content' group is situated at the top. $content = $this->t('Content', array(), array('context' => 'Entity type group')); - // We cast the optgroup label to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. $options = array((string) $content => $options[(string) $content]) + $options; } diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php index c3e2f68fea52c795b817508253e4b4e57a8f3159..836ad0b2833f7d57d922f64b4c89df83635b93a9 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php @@ -33,13 +33,24 @@ interface TranslationInterface { * what is used to display the page. * - 'context': The context the source string belongs to. * - * @return string + * @return string|\Drupal\Core\StringTranslation\TranslationWrapper * The translated string. * * @see \Drupal\Component\Utility\SafeMarkup::format() */ public function translate($string, array $args = array(), array $options = array()); + /** + * Translates a TranslationWrapper object to a string. + * + * @param \Drupal\Core\StringTranslation\TranslationWrapper $translated_string + * A TranslationWrapper object. + * + * @return string + * The translated string. + */ + public function translateString(TranslationWrapper $translated_string); + /** * Formats a string containing a count of items. * diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php index af6aa2bc210e97d4294c1c9ebc4d56bd845cfb29..5bf6a9d1eccd925ecf20848560acb7d14996b853 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -140,21 +140,27 @@ public function getStringTranslation($langcode, $string, $context) { * {@inheritdoc} */ public function translate($string, array $args = array(), array $options = array()) { - $string = $this->doTranslate($string, $options); - if (empty($args)) { - // We add the string to the safe list as opposed to making it an object - // implementing SafeStringInterface as we may need to call __toString() - // on the object before render time, at which point the string ceases to - // be safe, and working around this would require significant rework. - // Adding this string to the safe list is assumed to be safe because - // translate() should only be called with strings defined in code. - // @see \Drupal\Core\StringTranslation\TranslationInterface::translate() - SafeMarkup::setMultiple([$string => ['html' => TRUE]]); - return $string; - } - else { - return SafeMarkup::format($string, $args); + $safe = TRUE; + foreach (array_keys($args) as $arg_key) { + // If the string has arguments that start with '!' we consider it unsafe + // and return the translation as a string for backward compatibility + // purposes. + // @todo https://www.drupal.org/node/2570037 remove this temporary + // workaround. + if (0 === strpos($arg_key, '!') && !SafeMarkup::isSafe($args[$arg_key])) { + $safe = FALSE; + break; + } } + $wrapper = new TranslationWrapper($string, $args, $options, $this); + return $safe ? $wrapper : (string) $wrapper; + } + + /** + * {@inheritdoc} + */ + public function translateString(TranslationWrapper $translated_string) { + return $this->doTranslate($translated_string->getUntranslatedString(), $translated_string->getOptions()); } /** @@ -172,13 +178,11 @@ public function translate($string, array $args = array(), array $options = array * The translated string. */ protected function doTranslate($string, array $options = array()) { - // Merge in defaults. - if (empty($options['langcode'])) { - $options['langcode'] = $this->defaultLangcode; - } - if (empty($options['context'])) { - $options['context'] = ''; - } + // Merge in options defaults. + $options = $options + [ + 'langcode' => $this->defaultLangcode, + 'context' => '', + ]; $translation = $this->getStringTranslation($options['langcode'], $string, $options['context']); return $translation === FALSE ? $string : $translation; } diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php index 10e9b12165efd098176857b3ab87490550f0c757..7b4abfc5c59009594fcf4ee6dbbade34b4096e69 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -7,21 +7,25 @@ namespace Drupal\Core\StringTranslation; +use Drupal\Component\Utility\PlaceholderTrait; use Drupal\Component\Utility\SafeStringInterface; use Drupal\Component\Utility\ToStringTrait; /** - * Provides a class to wrap a translatable string. + * Provides translatable string class. * - * 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. + * This class delays translating strings until rendering them. * + * This is useful for using translation in very low level subsystems like entity + * definition and stream wrappers. + * + * @see \Drupal\Core\StringTranslation\TranslationManager::translate() + * @see \Drupal\Core\StringTranslation\TranslationManager::translateString() * @see \Drupal\Core\Annotation\Translation */ class TranslationWrapper implements SafeStringInterface { - use StringTranslationTrait; + use PlaceholderTrait; use ToStringTrait; /** @@ -31,6 +35,13 @@ class TranslationWrapper implements SafeStringInterface { */ protected $string; + /** + * The translated string without placeholder replacements. + * + * @var string + */ + protected $translatedString; + /** * The translation arguments. * @@ -45,6 +56,13 @@ class TranslationWrapper implements SafeStringInterface { */ protected $options; + /** + * The string translation service. + * + * @var \Drupal\Core\StringTranslation\TranslationInterface + */ + protected $stringTranslation; + /** * Constructs a new class instance. * @@ -57,11 +75,14 @@ class TranslationWrapper implements SafeStringInterface { * (optional) An array with placeholder replacements, keyed by placeholder. * @param array $options * (optional) An array of additional options. + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * (optional) The string translation service. */ - public function __construct($string, array $arguments = array(), array $options = array()) { + public function __construct($string, array $arguments = array(), array $options = array(), TranslationInterface $string_translation = NULL) { $this->string = $string; $this->arguments = $arguments; $this->options = $options; + $this->stringTranslation = $string_translation; } /** @@ -96,6 +117,17 @@ public function getOption($name) { public function getOptions() { return $this->options; } + + /** + * Gets all argments from this translation wrapper. + * + * @return mixed[] + * The array of arguments. + */ + public function getArguments() { + return $this->arguments; + } + /** * Renders the object as a string. * @@ -103,7 +135,18 @@ public function getOptions() { * The translated string. */ public function render() { - return $this->t($this->string, $this->arguments, $this->options); + if (!isset($this->translatedString)) { + $this->translatedString = $this->getStringTranslation()->translateString($this); + } + + // Handle any replacements. + // @todo https://www.drupal.org/node/2509218 Note that the argument + // replacement is not stored so that different sanitization strategies can + // be used in different contexts. + if ($args = $this->getArguments()) { + return $this->placeholderFormat($this->translatedString, $args); + } + return $this->translatedString; } /** @@ -123,5 +166,18 @@ public function jsonSerialize() { return $this->__toString(); } + /** + * Gets the string translation service. + * + * @return \Drupal\Core\StringTranslation\TranslationInterface + * The string translation service. + */ + protected function getStringTranslation() { + if (!$this->stringTranslation) { + $this->stringTranslation = \Drupal::service('string_translation'); + } + + return $this->stringTranslation; + } } diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index cc0d591d44543955c08a409c1e488ebaa6ec44c1..76d7c6a52bc4556e6632359e3506989e11033906 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -109,7 +109,8 @@ protected function createAttributeValue($name, $value) { elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); } - elseif (!is_object($value)) { + // As a development aid, we allow the value to be a safe string object. + elseif (!is_object($value) || $value instanceof SafeStringInterface) { $value = new AttributeString($name, $value); } return $value; diff --git a/core/lib/Drupal/Core/Validation/DrupalTranslator.php b/core/lib/Drupal/Core/Validation/DrupalTranslator.php index a2bbf5b580e8359b0d006ee45a4aab349203e8fb..ba1a1f966d57fd47b0a8458385d615ae86d7dc82 100644 --- a/core/lib/Drupal/Core/Validation/DrupalTranslator.php +++ b/core/lib/Drupal/Core/Validation/DrupalTranslator.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Validation; +use Drupal\Component\Utility\SafeStringInterface; + /** * Translates strings using Drupal's translation system. * @@ -73,8 +75,14 @@ public function getLocale() { protected function processParameters(array $parameters) { $return = array(); foreach ($parameters as $key => $value) { + // We allow the values in the parameters to be safe string objects. This + // can be useful when we want to use parameter values that are + // TranslationWrappers. + if ($value instanceof SafeStringInterface) { + $value = (string) $value; + } if (is_object($value)) { - // t() does not work will objects being passed as replacement strings. + // t() does not work with objects being passed as replacement strings. } // Check for symfony replacement patterns in the form "{{ name }}". elseif (strpos($key, '{{ ') === 0 && strrpos($key, ' }}') == strlen($key) - 3) { diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php index d873820b28edeb612b5c6958d9b3ab1fde63c337..6fa8771dd39d1a02661c6e423c853ccbd7f0cca0 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php @@ -16,6 +16,7 @@ use Drupal\Core\TypedData\Type\StringInterface; use Drupal\Core\TypedData\Type\UriInterface; use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait; +use Drupal\Component\Utility\SafeStringInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -49,7 +50,7 @@ public function validate($value, Constraint $constraint) { if ($typed_data instanceof IntegerInterface && filter_var($value, FILTER_VALIDATE_INT) === FALSE) { $valid = FALSE; } - if ($typed_data instanceof StringInterface && !is_scalar($value)) { + if ($typed_data instanceof StringInterface && !is_scalar($value) && !($value instanceof SafeStringInterface)) { $valid = FALSE; } // Ensure that URIs comply with http://tools.ietf.org/html/rfc3986, which diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index d5bb6906f7a110d326dae73b9467bd80cebe57e4..912076314e0512fc794ed731c489d0fca1f6e309 100644 --- a/core/modules/ckeditor/ckeditor.admin.inc +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -117,8 +117,6 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) { $variables['active_buttons'] = array(); foreach ($active_buttons as $row_number => $button_row) { foreach ($button_groups[$row_number] as $group_name) { - // We cast the group name to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. $group_name = (string) $group_name; $variables['active_buttons'][$row_number][$group_name] = array( 'group_name_class' => Html::getClass($group_name), diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 9240ce6ef36f66c40b2ac4a24b736e127ea1ff96..0756a5333b70f1d3cb38a73c94fafa71c1dff606 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -298,8 +298,6 @@ function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInt $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name')); } if (!_comment_entity_uses_integer_id($form_state->get('entity_type_id'))) { - // We cast the optgroup label to string as array keys must not be objects - // and t() will return a TranslationWrapper once issue #2557113 lands. $optgroup = (string) t('General'); // You cannot use comment fields on entity types with non-integer IDs. unset($form['add']['new_storage_type']['#options'][$optgroup]['comment']); diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module index 908e0e58de9bbfc03adaf1b2ada9e803047c0d78..ae4cc039c1046e101ccb703ddd916142164bca85 100644 --- a/core/modules/entity_reference/entity_reference.module +++ b/core/modules/entity_reference/entity_reference.module @@ -126,8 +126,6 @@ function entity_reference_field_config_presave(FieldConfigInterface $field) { * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'. */ function entity_reference_form_field_ui_field_storage_add_form_alter(array &$form) { - // We cast the optgroup label to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. $optgroup = (string) t('Reference'); // Move the "Entity reference" option to the end of the list and rename it to // "Other". diff --git a/core/modules/language/src/Form/NegotiationBrowserForm.php b/core/modules/language/src/Form/NegotiationBrowserForm.php index 111f35af5dfde134a02d7ff0cbdb1b30a8ccf112..5fa186a7ba58bd2d64e5f3fe5204c51dc63d75fd 100644 --- a/core/modules/language/src/Form/NegotiationBrowserForm.php +++ b/core/modules/language/src/Form/NegotiationBrowserForm.php @@ -82,8 +82,6 @@ public function buildForm(array $form, FormStateInterface $form_state) { } else { $language_options = array( - // We cast the optgroup labels to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. (string) $this->t('Existing languages') => $existing_languages, (string) $this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(), ); diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 80620a088583aff4f55f92ee2b95dd910ba11e2c..7259d35b1dba8483957a40152a25800b16e15249 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -641,7 +641,7 @@ function locale_form_language_admin_overview_form_alter(&$form, FormStateInterfa } } - array_splice($form['languages']['#header'], -1, 0, t('Interface translation')); + array_splice($form['languages']['#header'], -1, 0, ['translation-interface' => t('Interface translation')]); foreach ($languages as $langcode => $language) { $stats[$langcode] += array( diff --git a/core/modules/locale/src/Form/ImportForm.php b/core/modules/locale/src/Form/ImportForm.php index 86aecc34921e296fe9e41288e8c8254fb0a9f5e9..f3007f63e8391bbe91408ade4f1472f5486e62cd 100644 --- a/core/modules/locale/src/Form/ImportForm.php +++ b/core/modules/locale/src/Form/ImportForm.php @@ -94,8 +94,6 @@ public function buildForm(array $form, FormStateInterface $form_state) { else { $default = key($existing_languages); $language_options = array( - // We cast the optgroup labels to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. (string) $this->t('Existing languages') => $existing_languages, (string) $this->t('Languages not yet added') => $this->languageManager->getStandardLanguageListWithoutConfigured(), ); diff --git a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php index 6c35b1800f24169f1100296ef429de6bbee01f66..a6999b842433ea1aca17fa2975dda2ca84121862 100644 --- a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php +++ b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php @@ -64,7 +64,7 @@ public function testStringTranslation() { ); $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + t($name, array(), array('langcode' => $langcode))->render(); // Reset locale cache. $this->container->get('string_translation')->reset(); $this->assertRaw('"edit-languages-' . $langcode . '-weight"', 'Language code found.'); @@ -237,9 +237,10 @@ public function testJavaScriptTranslation() { // Retrieve the source string of the first string available in the // {locales_source} table and translate it. - $source = db_select('locales_source', 'l') - ->fields('l', array('source')) - ->condition('l.source', '%.js%', 'LIKE') + $query = db_select('locales_source', 's'); + $query->addJoin('INNER', 'locales_location', 'l', 's.lid = l.lid'); + $source = $query->fields('s', array('source')) + ->condition('l.type', 'javascript') ->range(0, 1) ->execute() ->fetchField(); @@ -302,7 +303,7 @@ public function testStringValidation() { ); $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + t($name, array(), array('langcode' => $langcode))->render(); // Reset locale cache. $search = array( 'string' => $name, @@ -361,7 +362,7 @@ public function testStringSearch() { $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + t($name, array(), array('langcode' => $langcode))->render(); // Reset locale cache. $this->container->get('string_translation')->reset(); $this->drupalLogout(); diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index eb1a80c66119e1470c091d8ac261b793b98e07d4..9ef0a662bb8d479091555970bca86108944cdae3 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -806,6 +806,8 @@ protected function assertTitle($title, $message = '', $group = 'Other') { preg_match('@<title>(.*)</title>@', $this->getRawContent(), $matches); if (isset($matches[1])) { $actual = $matches[1]; + $actual = $this->castSafeStrings($actual); + $title = $this->castSafeStrings($title); if (!$message) { $message = SafeMarkup::format('Page title @actual is equal to @expected.', array( '@actual' => var_export($actual, TRUE), diff --git a/core/modules/system/src/Tests/File/NameMungingTest.php b/core/modules/system/src/Tests/File/NameMungingTest.php index 10caf61bf89d432cd774f1be54e4c6ae9f52ab56..3afd2735d5cdc2dd91f8c0ec32dc68da1ae84e69 100644 --- a/core/modules/system/src/Tests/File/NameMungingTest.php +++ b/core/modules/system/src/Tests/File/NameMungingTest.php @@ -44,7 +44,7 @@ function testMunging() { $this->config('system.file')->set('allow_insecure_uploads', 0)->save(); $munged_name = file_munge_filename($this->name, '', TRUE); $messages = drupal_get_messages(); - $this->assertTrue(in_array(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $munged_name)), $messages['status']), 'Alert properly set when a file is renamed.'); + $this->assertTrue(in_array(strtr('For security reasons, your upload has been renamed to <em class="placeholder">%filename</em>.', array('%filename' => $munged_name)), $messages['status']), 'Alert properly set when a file is renamed.'); $this->assertNotEqual($munged_name, $this->name, format_string('The new filename (%munged) has been modified from the original (%original)', array('%munged' => $munged_name, '%original' => $this->name))); } diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php index 83b9129014a51f353e3bf172f9a5604257d71dac..389d23efe27fa647d338cc400bd9b6c19aadec85 100644 --- a/core/modules/system/src/Tests/Form/FormTest.php +++ b/core/modules/system/src/Tests/Form/FormTest.php @@ -144,7 +144,7 @@ function testRequiredFields() { // Select elements are going to have validation errors with empty // input, since those are illegal choices. Just make sure the // error is not "field is required". - $this->assertTrue((empty($errors[$element]) || strpos('field is required', $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element"); + $this->assertTrue((empty($errors[$element]) || strpos('field is required', (string) $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element"); } else { // Make sure there is *no* form error for this element. diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php index e435c0a22973903bc7e63c9a9eea5548b5b96902..f079785741d047b54af62652a2e842728855560a 100644 --- a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php +++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php @@ -78,7 +78,7 @@ public function __construct() { 'label' => t('User name'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User')), + 'user' => $this->createContextDefinition('entity:user', t('User')), ), )); @@ -87,7 +87,7 @@ public function __construct() { 'label' => t('User name optional'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User'), FALSE), + 'user' => $this->createContextDefinition('entity:user', t('User'), FALSE), ), )); @@ -102,8 +102,8 @@ public function __construct() { 'label' => t('Complex context'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockComplexContextBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User')), - 'node' => new ContextDefinition('entity:node', t('Node')), + 'user' => $this->createContextDefinition('entity:user', t('User')), + 'node' => $this->createContextDefinition('entity:node', t('Node')), ), )); @@ -118,4 +118,24 @@ public function __construct() { // specified), so we provide it the discovery object. $this->factory = new ReflectionFactory($this->discovery); } + + /** + * Creates a new context definition with a label that is cast to string. + * + * @param string $data_type + * The required data type. + * @param mixed string|null $label + * The label of this context definition for the UI. + * @param bool $required + * Whether the context definition is required. + * + * @return \Drupal\Core\Plugin\Context\ContextDefinition + */ + protected function createContextDefinition($data_type, $label, $required = TRUE) { + // We cast the label to string for testing purposes only, as it may be + // a TranslationWrapper and we will do assertEqual() checks on arrays that + // include ContextDefinition objects, and var_export() has problems + // printing TranslationWrapper objects. + return new ContextDefinition($data_type, (string) $label, $required); + } } diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 63a76b6b2e4dcc97aa1de6418e4386d9d99391d7..6f94d219a18db5230977307482c3cfd925f4aa05 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -140,6 +140,11 @@ function update_page_top() { $status = update_requirements('runtime'); foreach (array('core', 'contrib') as $report_type) { $type = 'update_' . $report_type; + // hook_requirements() supports render arrays therefore we need to render + // them before using drupal_set_message(). + if (isset($status[$type]['description']) && is_array($status[$type]['description'])) { + $status[$type]['description'] = \Drupal::service('renderer')->renderPlain($status[$type]['description']); + } if (!empty($verbose)) { if (isset($status[$type]['severity'])) { if ($status[$type]['severity'] == REQUIREMENT_ERROR) { diff --git a/core/modules/user/src/Tests/UserCancelTest.php b/core/modules/user/src/Tests/UserCancelTest.php index 535701340972c9fd7e66e1eaf8ea38cf5a556892..648f56ff57035dafacea5c15b5c139453301fe8c 100644 --- a/core/modules/user/src/Tests/UserCancelTest.php +++ b/core/modules/user/src/Tests/UserCancelTest.php @@ -535,7 +535,7 @@ function testMassUserCancelByAdmin() { $this->drupalPostForm(NULL, NULL, t('Cancel accounts')); $status = TRUE; foreach ($users as $account) { - $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->getUsername()))) !== FALSE); + $status = $status && (strpos($this->content, $account->getUsername() . '</em> has been deleted.') !== FALSE); $user_storage->resetCache(array($account->id())); $status = $status && !$user_storage->load($account->id()); } diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php index b8b7a30ed8286b354c66618ea843c3986792b0ba..cbe75261f39ffe5b565d532cec17347a3af271c5 100644 --- a/core/modules/views/src/Plugin/views/PluginBase.php +++ b/core/modules/views/src/Plugin/views/PluginBase.php @@ -14,6 +14,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase as ComponentPluginBase; use Drupal\Core\Render\Element; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ViewExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -561,7 +562,14 @@ protected function listLanguages($flags = LanguageInterface::STATE_ALL, array $c // Since this is not a real language, surround it by '***LANGUAGE_...***', // like the negotiated languages below. if ($flags & LanguageInterface::STATE_SITE_DEFAULT) { - $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $this->t($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName()); + $name = $languages[LanguageInterface::LANGCODE_SITE_DEFAULT]->getName(); + // The language name may have already been translated, no need to + // translate it again. + // @see Drupal\Core\Language::filterLanguages(). + if (!$name instanceof TranslationWrapper) { + $name = $this->t($name); + } + $list[PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT] = $name; // Remove site default language from $languages so it's not added // twice with the real languages below. unset($languages[LanguageInterface::LANGCODE_SITE_DEFAULT]); diff --git a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php index 2e4564e5da008546dd6aff919bd8de65a3050f07..1a97ee5191f52291aa635a6e10390899072581d8 100644 --- a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php +++ b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php @@ -52,8 +52,6 @@ public function tokenForm(&$form, FormStateInterface $form_state) { // Get a list of the available fields and arguments for token replacement. $options = array(); - // We cast the optgroup labels to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. $optgroup_arguments = (string) t('Arguments'); $optgroup_fields = (string) t('Fields'); foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) { diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 1bb339440a6ba0ab987478a37d0ab04c0d11b984..76ae608b1add19cd3c8eba7069dcb625ae032dcc 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -1725,9 +1725,6 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { ); $options = array(); - // We cast the optgroup label to string as array keys must not be - // objects and t() may return a TranslationWrapper once issue #2557113 - // lands. $optgroup_arguments = (string) t('Arguments'); foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', array('@argument' => $handler->adminLabel())); diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 29abf887c3008064ff47bbd1f5e04246df3a8328..bfdd1a021fb4fb2e639daff00a2426c05de1969b 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -862,8 +862,6 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { // Setup the tokens for fields. $previous = $this->getPreviousFieldLabels(); - // We cast the optgroup labels to string as array keys must not be objects - // and t() may return a TranslationWrapper once issue #2557113 lands. $optgroup_arguments = (string) t('Arguments'); $optgroup_fields = (string) t('Fields'); foreach ($previous as $id => $label) { diff --git a/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php b/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3ef61629274ad404bad2dd80e0b403159e2bbd6a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\KernelTests\Core\Common\DrupalSetMessageTest. + */ + +namespace Drupal\KernelTests\Core\Common; + +use Drupal\KernelTests\KernelTestBase; + +/** + * @covers ::drupal_set_message + * @group PHPUnit + */ +class DrupalSetMessageTest extends KernelTestBase { + + /** + * The basic functionality of drupal_set_message(). + */ + public function testDrupalSetMessage() { + drupal_set_message(t('A message: @foo', ['@foo' => 'bar'])); + $messages = drupal_get_messages(); + $this->assertInstanceOf('Drupal\Core\Render\SafeString', $messages['status'][0]); + $this->assertEquals('A message: bar', (string) $messages['status'][0]); + } + + public function tearDown() { + // Clear session to prevent global leakage. + unset($_SESSION['messages']); + parent::tearDown(); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php index be8cca3b8495f1044a5b37f78fb41259e913ca2b..f06254c2abbe6601d316f8f43a8ad234f4cc50eb 100644 --- a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php +++ b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php @@ -45,9 +45,6 @@ public function testGet(array $values, $expected) { $options = isset($values['context']) ? array( 'context' => $values['context'], ) : array(); - $this->translationManager->expects($this->once()) - ->method('translate') - ->with($values['value'], $arguments, $options); $annotation = new Translation($values); diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php index ac920882544500c1b88be9cd70254e89b7692a55..15991a563fefca6bd1ebf26fd3d710b9dc18e125 100644 --- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php @@ -71,11 +71,10 @@ protected function setupContextualLinkDefault() { */ public function testGetTitle() { $title = 'Example'; - $this->pluginDefinition['title'] = (new TranslationWrapper($title)) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper($title, [], [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated')); $this->setupContextualLinkDefault(); @@ -87,11 +86,10 @@ public function testGetTitle() { */ public function testGetTitleWithContext() { $title = 'Example'; - $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'), $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array('context' => 'context')) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated with context')); $this->setupContextualLinkDefault(); @@ -103,11 +101,10 @@ public function testGetTitleWithContext() { */ public function testGetTitleWithTitleArguments() { $title = 'Example @test'; - $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value'), [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array('@test' => 'value'), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example value')); $this->setupContextualLinkDefault(); diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php index 02d4578d44953e51484b262e0f70b993f841148a..3de7774a838633e978e663b779c16698cbb1a95b 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php @@ -83,11 +83,10 @@ protected function setupLocalActionDefault() { * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() */ public function testGetTitle() { - $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example', [], [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated')); $this->setupLocalActionDefault(); @@ -100,11 +99,10 @@ public function testGetTitle() { * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() */ public function testGetTitleWithContext() { - $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context'), $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array('context' => 'context')) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated with context')); $this->setupLocalActionDefault(); @@ -115,11 +113,10 @@ public function testGetTitleWithContext() { * Tests the getTitle method with title arguments. */ public function testGetTitleWithTitleArguments() { - $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'), [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example @test', array('@test' => 'value'), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example value')); $this->setupLocalActionDefault(); diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php index d4032eccc2334f7e09c607c08dbc24a7c2d3a711..7c9dc58ed614a4e2da0843eb733a329d5f93b3cf 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php @@ -233,11 +233,10 @@ public function testActive() { * @covers ::getTitle */ public function testGetTitle() { - $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example', [], [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated')); $this->setupLocalTaskDefault(); @@ -249,11 +248,10 @@ public function testGetTitle() { */ public function testGetTitleWithContext() { $title = 'Example'; - $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'), $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array('context' => 'context')) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example translated with context')); $this->setupLocalTaskDefault(); @@ -264,12 +262,10 @@ public function testGetTitleWithContext() { * @covers ::getTitle */ public function testGetTitleWithTitleArguments() { - $title = 'Example @test'; - $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) - ->setStringTranslation($this->stringTranslation); + $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'), [], $this->stringTranslation)); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array('@test' => 'value'), array()) + ->method('translateString') + ->with($this->pluginDefinition['title']) ->will($this->returnValue('Example value')); $this->setupLocalTaskDefault(); diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php index 53165c97c902d40a8da947f87152919b2b8f1a94..4efd7010f4f737675a3a53b5a606f066edcb9e85 100644 --- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php +++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\StringTranslation { use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\SafeStringInterface; use Drupal\Core\StringTranslation\TranslationManager; use Drupal\Tests\UnitTestCase; @@ -59,6 +60,44 @@ public function testFormatPlural($count, $singular, $plural, array $args = array $this->assertEquals(SafeMarkup::isSafe($result), $safe); } + /** + * Tests translation using placeholders. + * + * @param string $string + * A string containing the English string to translate. + * @param array $args + * An associative array of replacements to make after translation. + * @param string $expected_string + * The expected translated string value. + * @param bool $returns_translation_wrapper + * Whether we are expecting a TranslationWrapper object to be returned. + * + * @dataProvider providerTestTranslatePlaceholder + */ + public function testTranslatePlaceholder($string, array $args = array(), $expected_string, $returns_translation_wrapper) { + $actual = $this->translationManager->translate($string, $args); + if ($returns_translation_wrapper) { + $this->assertInstanceOf(SafeStringInterface::class, $actual); + } + else { + $this->assertInternalType('string', $actual); + } + $this->assertEquals($expected_string, $actual); + } + + /** + * Provides test data for translate(). + * + * @return array + */ + public function providerTestTranslatePlaceholder() { + return [ + ['foo @bar', ['@bar' => 'bar'], 'foo bar', TRUE], + ['bar !baz', ['!baz' => 'baz'], 'bar baz', FALSE], + ['bar @bar !baz', ['@bar' => 'bar', '!baz' => 'baz'], 'bar bar baz', FALSE], + ['bar !baz @bar', ['!baz' => 'baz', '@bar' => 'bar'], 'bar baz bar', FALSE], + ]; + } } class TestTranslationManager extends TranslationManager { diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php index 3facf382e4315d59c9d897154ee34740c95fb7fb..af5dce8c03ea7cd145628ea04acf3193e1003c0e 100644 --- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php +++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\StringTranslation; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; /** @@ -55,9 +56,11 @@ public function errorHandler($error_number, $error_message) { * @covers ::__toString */ public function testToString() { + $translation = $this->getMock(TranslationInterface::class); + $string = 'May I have an exception please?'; - $text = $this->getMockBuilder('Drupal\Core\StringTranslation\TranslationWrapper') - ->setConstructorArgs([$string]) + $text = $this->getMockBuilder(TranslationWrapper::class) + ->setConstructorArgs([$string, [], [], $translation]) ->setMethods(['_die']) ->getMock(); $text @@ -65,11 +68,12 @@ public function testToString() { ->method('_die') ->willReturn(''); - $translation = $this->prophesize(TranslationInterface::class); - $translation->translate($string, [], [])->will(function () { + $translation + ->method('translateString') + ->with($text) + ->willReturnCallback(function () { throw new \Exception('Yes you may.'); }); - $text->setStringTranslation($translation->reveal()); // We set a custom error handler because of https://github.com/sebastianbergmann/phpunit/issues/487 set_error_handler([$this, 'errorHandler']); diff --git a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php index 2bcb3185f65015d5b2cfbc70aa8286ba9f140c8e..0707fa3baf3c8285537f38cd225196bd73f7c8c7 100644 --- a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php +++ b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php @@ -16,6 +16,7 @@ use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraint; use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraintValidator; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Tests\UnitTestCase; /** @@ -63,6 +64,7 @@ public function provideTestValidate() { $data[] = [new IntegerData(DataDefinition::create('integer')), 1.5, FALSE]; $data[] = [new IntegerData(DataDefinition::create('integer')), 'test', FALSE]; $data[] = [new StringData(DataDefinition::create('string')), 'test', TRUE]; + $data[] = [new StringData(DataDefinition::create('string')), new TranslationWrapper('test'), TRUE]; // It is odd that 1 is a valid string. // $data[] = [$this->getMock('Drupal\Core\TypedData\Type\StringInterface'), 1, FALSE]; $data[] = [new StringData(DataDefinition::create('string')), [], FALSE]; diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php index 3433c942c00585e7c986e766ae724ca69983fdd8..e327a84be6fb30e681d3f06403cd88a4089555f8 100644 --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -12,6 +12,8 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Component\Utility\PlaceholderTrait; +use Drupal\Core\StringTranslation\TranslationWrapper; /** * Provides a base class and helpers for Drupal unit tests. @@ -20,6 +22,8 @@ */ abstract class UnitTestCase extends \PHPUnit_Framework_TestCase { + use PlaceholderTrait; + /** * The random generator. * @@ -214,7 +218,17 @@ public function getStringTranslationStub() { $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); $translation->expects($this->any()) ->method('translate') - ->will($this->returnCallback('Drupal\Component\Utility\SafeMarkup::format')); + ->willReturnCallback(function ($string, array $args = array(), array $options = array()) use ($translation) { + $wrapper = new TranslationWrapper($string, $args, $options, $translation); + // Pretend everything is not safe. + // @todo https://www.drupal.org/node/2570037 return the wrapper instead. + return (string) $wrapper; + }); + $translation->expects($this->any()) + ->method('translateString') + ->willReturnCallback(function (TranslationWrapper $wrapper) { + return $wrapper->getUntranslatedString(); + }); $translation->expects($this->any()) ->method('formatPlural') ->willReturnCallback(function ($count, $singular, $plural, array $args = [], array $options = []) {