Problem/Motivation

In #2114069: Routing / tabs / actions lack way to attach context to UI strings we are adding context to UI strings to handle the case of ambiguous items like "Extend" and "Views". However tabs / routes / actions may also equally need title replacement values. The menu system supported these so this is a regression that we only allow static strings. This results in problems that derivative tab titles, eg. 'Translate @type' don't work, where we'd derive @type from elsewhere. See #2119497-9: Default local tasks are not to be assumed $route_name . '_tab' point 1.

Proposed resolution

Add support for replacement arguments that routes/tabs/actions would be able to provide. This will round out the arguments of t() to be supported.

Remaining tasks

- Commit.

User interface changes

None.

API changes

- Local tasks / local actions / contextual links get a new title_arguments key that is an optional array of key-value pairs
- Routes get a new _title_arguments key (in defaults) that is an optional array of key-value pairs

These can be used to provide static title arguments for replacement after translation (as in Drupal 7 - resolves the Drupal 7 regression). Additional title arguments are added from the request raw variables as well.

#2114069: Routing / tabs / actions lack way to attach context to UI strings

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Gábor Hojtsy’s picture

Category: task » bug

Regression should be a bug.

Gábor Hojtsy’s picture

Gábor Hojtsy’s picture

Gábor Hojtsy’s picture

Issue tags: +sprint

Tagging for sprint.

dawehner’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
7.08 KB

Technically we can already do that if you override the class need for contextual links but yeah I agree that this should be part of the actual system, if D6/D7 supported it as well.

Gábor Hojtsy’s picture

Thanks a lot! I *thought* that allowing for @ placeholders only may be an issue, especially given D7 did not have any limitation like that:

    // t() is a special case. Since it is used very close to all the time,
    // we handle it directly instead of using indirect, slower methods.
    if ($callback == 't') {
      if (empty($item['title_arguments'])) {
        $item['title'] = t($item['title']);
      }
      else {
        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
      }
    }
    elseif ($callback && function_exists($callback)) {
      if (empty($item['title_arguments'])) {
        $item['title'] = $callback($item['title']);
      }
      else {
        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
      }
      // Avoid calling check_plain again on l() function.
      if ($callback == 'check_plain') {
        $item['localized_options']['html'] = TRUE;
      }
    }

But then again, this was for many things not "just" tabs/routes/etc. I did not find any processing of title arguments from the data returned in the hook to serializing it in the db in _menu_router_save(). So in D7 you could use @, ! or % equally. I think % would look pretty out of place in a contextual link or tab, but ! *may* be needed if you are working with already processed data. Allowing the full placeholder name would also have a transparent API without magic.

Is the concern that this will be harder to encode in YAML? Quotes needed around the keys due to the special chars? I think the use case of title placeholders in static YAML's is *minimal* if not nonexistent, since if you have a static tab/route, etc. you can just include the text in the title, right? :) Any other concerns with using the full placeholder name?

Gábor Hojtsy’s picture

Title: Regression: routing / tabs / actions lack way to attach replacement arguments to UI strings » Regression: routing / tabs / actions / contextual links lack way to attach replacement arguments to UI strings
Status: Needs review » Needs work
Gábor Hojtsy’s picture

#2130075: Config translation should work with new contextual links API demonstrates how we lost this functionality on contextual links (that was used by config_translation :).

dawehner’s picture

Well yeah we could scan the title for used "tokens" and than replace the values with the available request attributes.

Gábor Hojtsy’s picture

Status: Needs work » Needs review

5: title_parameters-2120235.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 5: title_parameters-2120235.patch, failed testing.

The last submitted patch, 5: title_parameters-2120235.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
Issue tags: +MenuSystemRevamp
FileSize
7.63 KB

It seems to be that something like this could already work.

dawehner’s picture

13: title-2120235-13.patch queued for re-testing.

Gábor Hojtsy’s picture

Status: Needs review » Needs work

The changes look good, thanks for working on this @dawehner. The only thing to note with the patch seems to be missing coverage for contextual links at this point. :) Looks RTBC except that :)

dawehner’s picture

Status: Needs work » Needs review
FileSize
14.5 KB
6.87 KB

Oh right, good point.
There we go.

Status: Needs review » Needs work

The last submitted patch, 16: title-2120235-16.patch, failed testing.

dawehner’s picture

16: title-2120235-16.patch queued for re-testing.

The last submitted patch, 16: title-2120235-16.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
15.57 KB
1.07 KB

ups.

tstoeckler’s picture

Could you explain how one would go about using this?

Using the example of 'Translate @type' from the issue summary it is not immediately clear to me how I would make the 'type' that I want end up in the request variables.

Also it is not super problematic but a bit unfortunate that this approach seemingly does not support translating 'This is escaped: @title, this is not escaped !title' (i.e. multiple placeholders of different type with the same name). I don't know any use-case for that I just think it's unfortunate that t() actually supports that but this does not. Perhaps that should be documented somewhere?

Gábor Hojtsy’s picture

@tstoeckler:

1. I think you can add arbitrary (static) request variables in the routing.yml file's defaults section. That is not a very creative use case, because then you can just as well include that in the title itself (since it would be static). However, for dynamically defined routes, you can add the placeholder values which are replaced later on in title translation. That means #2134077: Subclass contextual links and local tasks to have dynamic title can mostly be rolled back and we should be able to rely on this. I did not explicitly check the defaults values all end up in _raw_variables. I could not verify that with a quick search. Maybe would be best to convert that back here to serve as additional proof.

2. As for supporting !value and %value, that is already supported, all keys are added with all placeholder types to the replacement array.

tstoeckler’s picture

Re 1: So you mean on /node/123 because the route has a route parameter, I can do 'Node id: !node' as title?! I don't quite get what

However, for dynamically defined routes, you can add the placeholder values which are replaced later on in title translation.

means in practice.

Re 2: I meant the use-case of:

t('@foo is different from !foo', array('@foo' => $foo_1, '!foo' => $foo_2));

where the different placeholder types correspond do different variables. Again, I realize this is very far fetched, but it's inconsistent with the general usage of t().

Gábor Hojtsy’s picture

You can add them as different names easily.

dawehner’s picture

Maybe we should indeed also support direct placeholders like in D6/D7 where you could specify 'title arguments' in your hook_menu entry.

YesCT’s picture

Assigned: Unassigned » YesCT
Status: Needs review » Needs work
Issue tags: +Needs reroll
YesCT’s picture

Assigned: YesCT » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
15.66 KB

pretty easy one. dont need to read these notes (unless it fails, or you are just curious)

conflict:
ContextualLinkManager.php

<<<<<<< HEAD
    $this->moduleHandler = $module_handler;
    $this->alterInfo('contextual_links_plugins');
=======
    $this->requestStack = $request_stack;
    $this->alterInfo($module_handler, 'contextual_links_plugins');
>>>>>>> 20

#2053153: Allow contrib modules to provide plugins on behalf of optional modules added the moduleHandler, so keeping that. it did

-    $this->alterInfo($module_handler, 'menu_local_actions');
+    $this->alterInfo('menu_local_actions');

also, so keeping that change.

And the patch here just was adding
+ $this->requestStack = $request_stack;

So that means I resolved the conflict like:

    $this->moduleHandler = $module_handler;
    $this->requestStack = $request_stack;
    $this->alterInfo('contextual_links_plugins');

pretty straightforward.

ContextualLinkManagerTest conflict was:

<<<<<<< HEAD
=======
    $request_stack = new RequestStack();
    $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'requestStack');
    $property->setAccessible(TRUE);
    $property->setValue($this->contextualLinkManager, $request_stack);

    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');

>>>>>>> 20

I dont see any context lines changes... and the patch is just inserting lines. so I kept those inserted lines from the patch.

Status: Needs review » Needs work

The last submitted patch, 27: title-2120235-27.patch, failed testing.

YesCT’s picture

Status: Needs work » Needs review
FileSize
15.56 KB
776 bytes

The testbot errors looked like they might be a common error that some people had seen,
in irc
@larowlan helped me sort through this phpunit error.

tldr;

+++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php
@@ -123,6 +124,13 @@ protected function setUp() {
+    $property->setValue($this->contextualLinkManager, $request_stack);
+
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+

$this->moduleHandler =
doesn't need to be added here, as it is already set earlier.

=======

$ ./vendor/bin/phpunit --group Menu --strict
PHPUnit 3.7.21 by Sebastian Bergmann.

Configuration read from /Users/ctheys/foo/d82-project/d82/core/phpunit.xml.dist

...........F.F.....

Time: 2 seconds, Memory: 48.25Mb

There were 2 failures:

1) Drupal\Tests\Core\Menu\ContextualLinkManagerTest::testGetContextualLinksArrayByGroup
Expectation failed for method name is equal to <string:alter> when invoked at sequence index 1.
The expected invocation at index 1 was never reached.


2) Drupal\Tests\Core\Menu\ContextualLinkManagerTest::testPluginDefinitionAlter
Expectation failed for method name is equal to <string:alter> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.


FAILURES!
Tests: 19, Assertions: 76, Failures: 2.

with the patch applied, http://privatepaste.com/7a44e42688#
looking for expects dealing with alter, found two moduleHandler expects:
$this->moduleHandler->expects($this->at(1)) ->method('alter') ->with($this->equalTo('contextual_links'), new \PHPUnit_Framework_Constraint_Count(2), $this->equalTo('group1'), $this->equalTo(array('key' => 'value')));
$this->moduleHandler->expects($this->once()) ->method('alter') ->with('contextual_links_plugins', $definitions);
so, were they trying to give the expected value (or something) for the first $this->moduleHandler =
and then the at(1) was what was expected for the second call?
or was it a mistake to do $this->moduleHandler = ... twice?

taking out the $this->moduleHandler =
that this patch added gave:
OK (19 tests, 79 assertions)

better.

patch attached.

Gábor Hojtsy’s picture

@dawehner: re#26 it is not entirely clear to me what is included with raw variables. Is it possible to inject anything in there with the routing structure or would we need specific support for that? Again, this issue is to avoid custom subclassing of subtabs and contextual links and have translation support built-in for placeholders to get back what we had in D7 without each module needing to subclass it for their own. So would this in itself be able to get rid of the subclassing in config translation?

dawehner’s picture

@dawehner: re#26 it is not entirely clear to me what is included with raw variables. Is it possible to inject anything in there with the routing structure or would we need specific support for that? Again, this issue is to avoid custom subclassing of subtabs and contextual links and have translation support built-in for placeholders to get back what we had in D7 without each module needing to subclass it for their own. So would this in itself be able to get rid of the subclassing in config translation?

So we talk about the following piece of code:

    $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
    return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);

So we could have something like the following in the yml files / the derivative definition:

  _title: Translate @type_name
  _title_arguments:
    type_name: Example
Gábor Hojtsy’s picture

Status: Needs review » Needs work

Yes, yes and yes :) The use cases are at least in the config translation module.

dawehner’s picture

Status: Needs work » Needs review
FileSize
5.89 KB
18.17 KB

Tryed(trait) to move the common code into a trait but it somehow did not worked. If someone wants to try it, feel free to do it!

Gábor Hojtsy’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Yay, that looks great to me. Thanks so much for resolving this!

Also updated issue summary, explained the API changes proposed.

Gábor Hojtsy’s picture

33: title-2120235-33.patch queued for re-testing.

catch’s picture

Status: Reviewed & tested by the community » Needs review

If it's not straighforward to add a trait, is there somewhere we can put a static method with a @todo for the trait?

dawehner’s picture

FileSize
18.28 KB

Sure.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

Yay, thanks!

catch’s picture

Status: Reviewed & tested by the community » Needs review

That adds a @todo to one case of the code, but I was asking about a static method somewhere with a @todo - to avoid the copy/paste.

dawehner’s picture

FileSize
15.33 KB
6.51 KB

Realized that we don't need dynamic parameters as this is already covered by using a custom getTitle method on your plugin.

Let's just support static title arguments, like we did in D7. This at least solves the regression and covers most of the usecases.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

So now the previously duplicate logic is only in the route titleresolver, and I wrote a change notice draft at [#2226891]. This should be all ready now.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.x, thanks!

  • Commit 2911062 on 8.x by catch:
    Issue #2120235 by dawehner, YesCT: Regression: routing / tabs / actions...
Gábor Hojtsy’s picture

Issue tags: -sprint

Woot, thanks @dawehner for your persistence.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

pwolanin’s picture

Status: Closed (fixed) » Active

I missed this when it went in, but the idea of putting static arguments in the YAML perplexes me. I don't understand the actual use case.

Gábor Hojtsy’s picture

The use case was/is to be able to drop the custom implementations is ConfigTranslationContextualLink and ConfigTranslationLocalTask, because the only reason those exist is due to the replacement strings (D7 title arguments equivalents) not present in Drupal 8. We did not do the conversion here, so maybe less evident.

pwolanin’s picture

@Gabor: So both of those are using title_context, which seems more reasonable since that would be a static string. I'm much less sure it makes sense to ever define title arguments in the plugin definition.

Gábor Hojtsy’s picture

Both of them also use a title argument that is derived from the plugin definition (so I think can just as well be stored in the plugin definition):

    $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
    return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);

Title context and title arguments were both added so to not need to override plugins for this case to avoid lots of contrib copy-paste implementations.

pwolanin’s picture

Status: Active » Closed (fixed)

Ok, I see: thee use case for this is really when creating derivatives, where each derivative would have a different static title argument.

I don't love it as a solution, but at least I see the use case now.

Gábor Hojtsy’s picture

#2252735: Convert contextual links title arguments to native title arguments shows the benefit with a diffstat of 4 files changed, 2 insertions(+), 110 deletions(-) :) Please review.

YesCT’s picture

+++ b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
@@ -16,13 +17,21 @@ class ContextualLinkDefault extends PluginBase implements ContextualLinkInterfac
-  public function getTitle() {
+  public function getTitle(Request $request = NULL) {

Why was this changed?

#2783375: ContextualLinkManager calls getTitle() on ContextualLinkInterface plugins without using the controller resolver is related