diff --git a/core/includes/common.inc b/core/includes/common.inc index b11a6a6..44a35b4 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; @@ -316,7 +317,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) { @@ -325,16 +326,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; @@ -343,7 +335,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 c845c4b..f673752 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 0000000..0592c37 --- /dev/null +++ b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php @@ -0,0 +1,61 @@ + $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] = '' . $value . ''; + 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 3020bbe..00b09f2 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] = '' . $value . ''; - 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 dd3c8f0..9d749fb 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 c3e2f68..836ad0b 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php @@ -33,7 +33,7 @@ * 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() @@ -41,6 +41,17 @@ 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. * * This function ensures that the string is pluralized correctly. Since t() is diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php index af6aa2b..5bf6a9d 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 10e9b12..7005657 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -7,20 +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 PlaceholderTrait; use StringTranslationTrait; use ToStringTrait; @@ -32,6 +37,13 @@ class TranslationWrapper implements SafeStringInterface { protected $string; /** + * The translated string without placeholder replacements. + * + * @var string + */ + protected $translatedString; + + /** * The translation arguments. * * @var array @@ -57,11 +69,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(), \Drupal\Core\StringTranslation\TranslationInterface $string_translation = NULL) { $this->string = $string; $this->arguments = $arguments; $this->options = $options; + $this->stringTranslation = $string_translation; } /** @@ -96,6 +111,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 +129,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 +160,4 @@ public function jsonSerialize() { return $this->__toString(); } - } diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index cc0d591..76d7c6a 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 a2bbf5b..ba1a1f9 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 d873820..6fa8771 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 d5bb690..9120763 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 edd20a0..8bc499f 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -299,8 +299,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 908e0e5..ae4cc03 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 111f35a..5fa186a 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 80620a0..7259d35 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 86aecc3..f3007f6 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 6c35b18..a6999b8 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 87e8887..d0b96ca 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('@