diff --git a/core/lib/Drupal/Component/Serialization/Yaml.php b/core/lib/Drupal/Component/Serialization/Yaml.php index 5cff6dd53f1ed1584710b81fbc067bde6827db07..6f93ab9193581df8ad709499347f94e82dad6f1b 100644 --- a/core/lib/Drupal/Component/Serialization/Yaml.php +++ b/core/lib/Drupal/Component/Serialization/Yaml.php @@ -5,6 +5,7 @@ use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Symfony\Component\Yaml\Dumper; use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Tag\TaggedValue; use Symfony\Component\Yaml\Yaml as SymfonyYaml; /** @@ -51,4 +52,39 @@ public static function getFileExtension() { return 'yml'; } + /** + * Parses custom YAML tags and convert values to primitive types. + * + * Tag objects will be replaced by their raw value or, if a callback + * exists for that tag it will be used. + * + * The only tag custom tag supported by default is the '!translate' tag. + * + * @param mixed $data + * Parsed YAML data. + * @param callable[] $tag_callbacks + * Optional callbacks for tag value, indexed by tag. + * + * @return mixed + * Parsed data with parsed tag values. + */ + public static function decodeTags($data, array $tag_callbacks = []) { + if (is_array($data)) { + foreach ($data as $key => $value) { + if (is_object($value) && $value instanceof TaggedValue) { + if (isset($tag_callbacks[$value->getTag()])) { + $data[$key] = call_user_func($tag_callbacks[$value->getTag()], $value); + } + else { + $data[$key] = $value->getValue(); + } + } + elseif (is_array($value)) { + $data[$key] = static::decodeTags($value, $tag_callbacks); + } + } + } + return $data; + } + } diff --git a/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php b/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php index f1db3a342af1e1f9b33cedd9b715b18cbfd410f2..fa76bea44e0c3c2ea279b3b2aca7792be833eebc 100644 --- a/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php +++ b/core/lib/Drupal/Core/Recipe/ConsoleInputCollector.php @@ -102,6 +102,11 @@ public function collectValue(string $name, DataDefinitionInterface $definition, // input definitions should define constraints. unset($arguments['validator']); + // Run localization before calling final method. + $arguments = array_map(function ($value) { + return is_object($value) && $value instanceof \Stringable ? (string) $value : $value; + }, $arguments); + return $this->io->$method(...$arguments); } diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php index 888f54e4f42cfdea0afd0142c1c011dfa824efa3..a83909ac60c1fddf27a4ef8ee882a56f54ab1bd0 100644 --- a/core/lib/Drupal/Core/Recipe/Recipe.php +++ b/core/lib/Drupal/Core/Recipe/Recipe.php @@ -11,6 +11,7 @@ use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Render\Element; +use Drupal\Core\StringTranslation\YamlTagTranslator; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Validation\Plugin\Validation\Constraint\RegexConstraint; use Symfony\Component\Validator\Constraints\All; @@ -191,7 +192,7 @@ private static function parse(string $file): array { fields: [ // Every input definition must have a description. 'description' => [ - new Type('string'), + new Type(['string', \Stringable::class]), new NotBlank(), ], // There can be an optional set of constraints, which is an @@ -309,6 +310,10 @@ private static function parse(string $file): array { ]); $recipe_data = Yaml::decode($recipe_contents); + + // Replace 'translate' tags with proper translations. + $recipe_data = Yaml::decodeTags($recipe_data, ['translate' => YamlTagTranslator::TranslateTaggedValue(...)]); + /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */ $violations = Validation::createValidator()->validate($recipe_data, $constraints); if (count($violations) > 0) { @@ -322,6 +327,7 @@ private static function parse(string $file): array { 'config' => [], 'content' => [], ]; + return $recipe_data; } diff --git a/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php b/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php index 5f8d8dd32d5ed0acf185bf85cb265d9ce3749970..850f8882eb27a20c5183627c286192148ca857ab 100644 --- a/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php +++ b/core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.php @@ -43,7 +43,7 @@ protected function buildRecipeInputForm(Recipe $recipe): array { * 'recipe_1' => [ * 'input_1' => [ * '#type' => 'textfield', - * '#title' => 'Some input value', + * '#title' => !translate 'Some input value', * ], * 'input_2' => [ * '#type' => 'checkbox', @@ -53,7 +53,7 @@ protected function buildRecipeInputForm(Recipe $recipe): array { * 'dependency_recipe' => [ * 'input_1' => [ * '#type' => 'textarea', - * '#title' => 'An input defined by a dependency of recipe_1', + * '#title' => !translate 'An input defined by a dependency of recipe_1', * ], * ], * '#tree' => TRUE, diff --git a/core/lib/Drupal/Core/StringTranslation/YamlTagTranslator.php b/core/lib/Drupal/Core/StringTranslation/YamlTagTranslator.php new file mode 100644 index 0000000000000000000000000000000000000000..ba5b1bedb4ec868d4d72bc58c7a6d5717fb26bc9 --- /dev/null +++ b/core/lib/Drupal/Core/StringTranslation/YamlTagTranslator.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\Core\StringTranslation; + +use Drupal\Component\Serialization\Exception\InvalidDataTypeException; +use Symfony\Component\Yaml\Tag\TaggedValue; + +/** + * Provides a callback for YAML 'translate' tags. + */ +class YamlTagTranslator { + + /** + * Callback for 'translate' tag object. + * + * A translatable string can be represented as a plain string, or as an array + * with 'string' and 'context' and maybe other parameters. For example: + * + * @code + * element: + * label: !translate 'Website feedback' + * another: + * label: !translate {string: 'Website feedback', context: 'Contact form'} + * @endcode + * + * In this second case, the full array minus the 'string' part will be passed + * as the '$options' parameter for the translation. + * + * @see t() + * + * @param \Symfony\Component\Yaml\Tag\TaggedValue $tagged_value + * The tagged value object. + * @param array $options + * Extra options to pass to the translation. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The translatable string object. + */ + public static function translateTaggedValue(TaggedValue $tagged_value, $options = []) { + $value = $tagged_value->getValue(); + if (is_array($value) && isset($value['string'])) { + $string = $value['string']; + unset($value['string']); + $options += $value; + } + elseif (is_string($value)) { + $string = $value; + } + else { + throw new InvalidDataTypeException("Cannot parse the 'translate' tag. The tagged value is not a string nor has a 'string' element."); + } + + return new TranslatableMarkup($string, [], $options); + } + +} diff --git a/core/recipes/core_recommended_admin_theme/recipe.yml b/core/recipes/core_recommended_admin_theme/recipe.yml index 61cd28e17c9c231e520cc9a47b98e8e01f2ccfed..dc134ac9fda65fb15ed1f0feda00a83df2579254 100644 --- a/core/recipes/core_recommended_admin_theme/recipe.yml +++ b/core/recipes/core_recommended_admin_theme/recipe.yml @@ -27,7 +27,7 @@ config: plugin: page_title_block settings: id: page_title_block - label: 'Page title' + label: !translate 'Page title' label_display: '0' provider: core setRegion: header diff --git a/core/recipes/core_recommended_front_end_theme/recipe.yml b/core/recipes/core_recommended_front_end_theme/recipe.yml index 00faf6409ec5493024559da9cddc8a37ac07f988..fcd5cebb0f614826cca439c59b5667b0d5c1193a 100644 --- a/core/recipes/core_recommended_front_end_theme/recipe.yml +++ b/core/recipes/core_recommended_front_end_theme/recipe.yml @@ -31,7 +31,7 @@ config: plugin: system_messages_block settings: id: system_messages_block - label: 'Status messages' + label: !translate 'Status messages' label_display: '0' provider: system setRegion: highlighted @@ -43,7 +43,7 @@ config: plugin: page_title_block settings: id: page_title_block - label: 'Page title' + label: !translate 'Page title' label_display: '0' provider: core setRegion: content_above diff --git a/core/recipes/feedback_contact_form/recipe.yml b/core/recipes/feedback_contact_form/recipe.yml index c6bf74cf40c5a69a63b5123d9268b98794ba7558..ff10fe500d4a81e98ef5fbfb001ceac5f917cb4b 100644 --- a/core/recipes/feedback_contact_form/recipe.yml +++ b/core/recipes/feedback_contact_form/recipe.yml @@ -6,16 +6,16 @@ install: input: recipient: data_type: email - description: 'The email address that should receive submissions from the feedback form.' + description: !translate 'The email address that should receive submissions from the feedback form.' constraints: NotBlank: [] prompt: method: ask arguments: - question: 'What email address should receive website feedback?' + question: !translate 'What email address should receive website feedback?' form: '#type': email - '#title': 'Feedback form email address' + '#title': !translate 'Feedback form email address' default: source: config config: ['system.site', 'mail'] @@ -30,8 +30,8 @@ config: actions: contact.form.feedback: createIfNotExists: - label: 'Website feedback' - message: 'Your message has been sent.' + label: !translate 'Website feedback' + message: !translate {string: 'Your message has been sent.', context: 'Contact form'} redirect: '' setRecipients: - ${recipient}