Problem/Motivation
Currently whenever we need to use string translation in class context, the class needs to have a t() method and call $this->t() otherwise our string extractor won't be able to find the strings (unless of course you use the global/procedural t() function, but that makes your code untestable)
Proposed resolution
Instead of requiring to write a t() method over and over again whenever you need the translation and your base class does not provide it, use a trait!
Remaining tasks
Write the trait
Review/RTBC
- commit
User interface changes
None
API changes
New trait available: Drupal\Core\StringTranslation\StringTranslationTrait
Original report by @chx
Given that $this->t() is mandatory we should provide a Trait -- core won't use it but having it documented in the codebase is better than burying this in a change notice.
Comments
Comment #1
gábor hojtsyTagging up.
Comment #2
dawehner+1
We could think about other onces as well.
Comment #3
ParisLiakos commentedoh, forgot this one.
here it is with a test. yay for the first trait in core!
Comment #5
ParisLiakos commentedsome nits..well i guess if we get this in, it wont break anything since
php -lonly run on files touched by the patch right?Comment #6
chx commentedAye, there's core/vendor/psr/log/Psr/Log/LoggerTrait.php, core/vendor/psr/log/Psr/Log/LoggerAwareTrait.php, and a couple tests.
Comment #7
gábor hojtsyLet's celebrate this would be the first *added by Drupal core*, ie. not a vendor :) *party*
Comment #8
robloachQuick note that you could actually accomplish what this does with a class...
If you want to get extra fancy, could even throw a StringTranslationInterface in there so that you're not dependent on the implementation. Food for thought. But, I'm likely completely missing the goal of this.
Comment #9
chx commentedPHP is single hierarcy so we can't use one class just for a few utility methods. An interface for t() might be workable, true.
Edit: however the implementation won't vary from class to class so a Trait is very helpful too.
Comment #10
ParisLiakos commentedan interface wont work cause t() is protected. and i doubt making it public is a good idea
Comment #11
effulgentsia commentedWhy did we ever introduce $this->t() into Drupal 8 to begin with? It's resulted in controllers calling it, but non-controller classes calling Drupal::t() or using some other crazily verbose pattern. Is there any actual use case for swapping out the translation manager used by one object separately from just swapping out the one in the DIC? Given that traits aren't available for D8, I suggest we step back and become more practical, and use the $this->SERVICE() pattern only for services that have a practical reason for being injected, and otherwise, stick to \Drupal::SERVICE().
In D9, when we can use traits, I think the $this->SERVICE() pattern for everything will be great.
Comment #12
webchickI'm pretty sure "BECAUSE UNIT TESTING" but if we can get away with only doing injections on things that have a pragmatic reason to be swappable, that's far, far preferable to me, too.
Comment #13
ParisLiakos commentedthere is no Drupal::t(). it is either t() or $this->t(). anything else is a bug
Background : #2018411: Figure out a nice DX when working with injected translation
Comment #14
effulgentsia commentedI'd be fine with just t() :)
But I'm also making a pitch to use PHP 5.4 and traits in D8: #1498574-69: [policy, no patch] Require PHP 5.4.
Comment #15
robloachI see two steps?
t()but make it justreturn Drupal::t($stuff)Reasoning for above:
t()is not available$this->t()is just a wrapper aroundDrupal::service('string_translation')->translate()anyway. Let's stop beating around the bush. There is no reason to complicate it more than it needs to be.$this->t()doesn't solve anything."Because Unit Testing" is actually just a ploy to move things into the class loader space, but don't tell anyone that.
Comment #16
Crell commentedRob: No, it's a ploy to make using PHPUnit easier/saner.
I'm favorable toward this sort of trait, as it has a nice balance between pragmatism (I just want to be able to call $this->t(), screw wiring up a service) with cleanliness and testability (I can do a true-unit test without much hoop jumping). Even if we don't require PHP 5.4, having a few of these is a good thing (and helps push people to use 5.4 in contrib!)
Shouldn't $translationManager be defined in the trait, though? If not, it becomes a dynamic public variable, which is leaky and uses more memory than a defined property. (I think traits can define properties, no?)
Comment #17
ParisLiakos commentedwhy the \Drupal::t() thing is worse than what we have now:
1st: It requires me to set up a container anytime i want to unit test a class that uses t(). I have to type more things to do this instead of just doing
2nd and more importanly: There are side effects with other unit tests..you have to kill the container before ending the test to make sure you wont screw other unit tests, that have to use the container too.
No thank you, i would rather we require 5.4
You are completely right..lets do this and also get rid one of the testT() methods:P
Comment #18
dawehnerIf this is just for tests place it in some test directory or maybe even better put it into the actual test file.
Comment #19
ParisLiakos commentedwe cant put it in the same file cause that would cause tests to fail in 5.3.
the same will happen if we move it in tests/ namespace..seems the autoloader picks it up..i just tried it in a 5.3 install, so having it in lib/ namespace is the only viable approach for now.
Comment #20
ParisLiakos commentedduh, i forgot to remove the attachments. disregard patch above, like i said it will cause tests to fail under 5.3.
re-uploading #17
Comment #21
jhedstromSince #2018411: Figure out a nice DX when working with injected translation is fixed, is this the place to remove http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/core/modul...?
Comment #22
dawehnerNot really to be honest. Please open another issue.
Comment #23
tim.plunkettBecause the impact of this change is not immediately clear, I've expanded it to actually show what core would look like if #1498574: [policy, no patch] Require PHP 5.4 went in.
Not counting the tests:
109 insertions(+), 330 deletions(-)
Not counting the tests or trait:
44 insertions(+), 330 deletions(-)
Comment #24
tim.plunkettDries has made a decision on #1498574: [policy, no patch] Require PHP 5.4, we just need to wait for the testbots now.
Comment #25
tim.plunkettComment #26
xanoComment #27
amateescu commentedRerolled and cleaned up a little.
Comment #28
amateescu commentedAs discussed in IRC, services should still receive the string_translation service injected and set it on the trait.
Comment #29
effulgentsia commentedInstead of all that, can we instead use setter injection everywhere that needs string_translation injected? It results both in less code in the class, and per #2019055-187: Switch from field-level language fallback to entity-level language fallback, makes subclasses less brittle.
In general, I would recommend only using constructor injection for injecting services that are close collaborators for the domain logic of the class, not for general services like string translation.
Comment #30
gábor hojtsyI think this would need format plural coverage as well now that that is a service.
Comment #31
ParisLiakos commentedtime for some testbot action here again :)
the patch was severily outdated, rerolled what i could.
@Gabor: agreed, but it might be good to split this to another issue if it requires many changes..i ll check and open a new issue, if they make this patch really big
Comment #33
ParisLiakos commentedOops
Comment #35
ParisLiakos commentedsetter should return $this
Comment #36
tstoecklerLooks great from a cursory review, awesomesauce. This is very exciting :-)
Could use another pair of eyes, though.
Comment #38
ParisLiakos commentedyes, we need to do formatPlural here too
Comment #39
ParisLiakos commentedi actually like stringTranslation better for property name, since thats the name of the service.
translationManager is the name of the class, which if you swap it, it wont be correct anymore
Comment #40
dawehnerActually the documentation should be on TranslationINterface::translate, shouldn't it?
Needs an inheritdoc, because it is people will always look at it.
You can also use $this->getObjectForTrait
Comment #41
ParisLiakos commentedFixed #39
#40:
1. Because the t() docs are far richer:)
2. Done
3. Didnt know about that..Awesomeness!
Comment #42
ParisLiakos commentedsome doc love
Comment #43
xanoI corrected some errors in the code comments and made them a bit easier to understand, mostly by moving around the subordinate clause. I also added PHPUnit annotations to the test, and clarified the test metadata. For approval on the format of that last bit, see @dawehner. The trait itself looks good and is otherwise very clearly documented.
Comment #45
yesct commentedI think description is required.
https://api.drupal.org/api/drupal/core%21tests%21Drupal%21Tests%21UnitTe...
Probably an accidental delete of that line.
Comment #46
xano'scuse me.
Comment #47
ParisLiakos commentedthanks for the annotations
this change is just wrong:)
Why we need the full namespace in the name that appears in the simpletest UI?
Also the description should not be empty.
Please revert, thanks
Comment #48
ParisLiakos commentedrenamed the trait to
StringTranslationAwareTraitand fixed #47sorry i messed up interdiff
Comment #49
dawehnerMaybe it is just me but I would still love to have proper constructor based DI for this really important services.
Comment #50
ParisLiakos commentedthose services dont need the translation..they are just dead properties:)
Comment #51
tim.plunkettActually, both of those services were only using it because the base class (HtmlControllerBase) had it in the constructor.
But neither that class, nor these services, actually use it.
You'll see no trait is added there, it's just an unrelated cleanup.
But yes, I would generally like to avoid using traits for our true services, and stick to DI. That's more for the "trait strategy" issue though.
Comment #52
dawehnerWell, at least the exception controller will need a way to translate things in the future, as it adds stuff. (just saying)
Comment #53
jhodgdonThere's an issue
#2206175: Document traits that use methods on interfaces
which is proposing a standard for how to document Traits.
The suggestions there (not official standards yet but probably a good idea):
a) Add a @see in the main Trait doc block referencing the interface whose methods it is implementing.
b) Document each method as:
Comment #54
ParisLiakos commentedthe trait in this issue does not implement any interface:)
Comment #55
jhodgdonAh, that's interesting. Let's discuss that possibility on the "how to document" issue.
Comment #56
jibranWell #49 and #51 is discussed in #2134513: [policy, no patch] Trait strategy. Doc standard is discussed in #2206175: Document traits that use methods on interfaces. Anything left other then RTBC because I haven't found anything related to doc or coding standard.
Comment #57
jhodgdonThe documentation of this trait needs a bit of work:
I'm not sure if this doc block was meant to be all one paragraph or multiple paragraphs? If it's all one, please wrap as a single paragraph. If it's two, leave blank lines between them.
I also don't really think this documentation makes a lot of sense. What is it really trying to say?
I also really really don't like the name of this trait "StringTranslationAwareTrait" ?!?? It seems to imply that it's providing "translation awareness", but isn't it really providing the translation service as a whole to classes that use the trait? Why was it renamed in #48 with no discussion as far as I can tell?
Comment #58
ParisLiakos commentedYes, the class that uses this trait, is aware of the string translation service. The trait doesn't provide the service, but it provides a setter method, a property to store it (Like ContainerAware) and wrapper methods for the service's methods. If the class is not aware of the string translation it will blow up. (also inspired by the LoggerAwareTrait in vendor/psr)
I am open to any renames though.
Comment #59
Crell commented"Aware" is not something I'd put on a trait, probably. Remember, this is a trait we're expecting people to use... a lot.
StringTranslationTrait or even just TranslationTrait seems fine to me.
Comment #60
ParisLiakos commentedi disagree. The "Aware" in the name informs me what this trait does pretty much. Without it i have to check the source code.
It also avoids potential name collisions with potential trait we want to add in the future for TranslationInterface implementations. (eg check
LoggerTraitvsLoggerAwareTraitinPsr/Log)But yes, maybe "StringTranslationAwareTrait" is too verbose.."TranslationAwareTrait" probably would be the best.
And when using it, it makes perfect sense:
Which means that MyClass is aware of the translation service and can translate strings using it.
Exactly like you would use
LoggerAwareTraitmeans that your class is aware of the logger service and can log stuff using it.Comment #61
jhodgdonWhat does "being aware of the translation service" mean? If I have a class, I want to translate text, not be aware of a service. I also think "LoggerAwareTrait" is not a good name. Maybe both of them should be called SomethingServiceTrait, meaning that the trait provides access to the service?
Comment #62
Crell commentedLoggerAwareTrait from PSR-3 doesn't auto-wrap a service locator. It wraps a property and implements a standard method for setter-injection. In that sense it's the same as Symfony's ContainerAwareInterface and base class.
What we're talking about here isn't primarily a standardized setter injection method; it's a service locator wrapper. That it also includes a setter method for testability is incidental.
Comment #63
ParisLiakos commentedok. i don't want to hold up the issue. lets go with the simplest approach here, and we can revisit if needed:)
Comment #65
tstoecklerLet's please stick to StringTranslation, though. There are various translation concepts in Drupal core right now (content translation, config translation, ...), so let's not be ambiguous here.
Comment #66
ParisLiakos commentedok rerolled and renamed.
Also removed the newlines between the class declaration and trait's use statement to match what we do already in core.
No interdiff, i messed it up.
I hope the exception of #63 goes away cause i couldnt reproduce locally
Comment #67
ParisLiakos commentedComment #68
jibranI love this patch let's get this in.
more then 80 chars.
Comment #69
ParisLiakos commentedoops indeed! also wrong namespace for the formatPlural method
Comment #70
ParisLiakos commentedreroll.
Comment #72
ParisLiakos commentedComment #73
ParisLiakos commentedforgot interdiff
Comment #75
ParisLiakos commentedComment #76
ParisLiakos commentedreroll...
Comment #78
ParisLiakos commentedoops messed up the arguments when merging
Comment #79
damien tournoud commentedThis is all very ugly. But if you really want to go that route, please make all those methods private, not protected.
Those are strictly helper functions (and as such, they should really not even be on the class to begin with...), they are clearly not part of the API that the class provides to its subclasses.
Comment #80
tim.plunkett@Damien Tournoud, so when someone subclasses FormBase, itself an abstract class, then no actual form would be able to use $this->t()? Or they themselves would have to use the trait, lessening the usefulness of the base class.
+1 for leaving it as protected, it's a helper, let's be helpful.
Comment #81
tstoecklerYes?! As far as I'm concerned that's the whole point of this issue. If your a form/controller/... and you need string translation ->
use StringTranslationTrait;. I don't see how that lessens the usefulness of the base class. If the whole purpose of the base class is currently to aggregate various services into utility methods (basically ControllerBase/FormBase/PluginBase now) but there is no domain-specific "base-logic" anyway, then once we have converted all utility services to traits then that base class has no right to existance anyway. If that's what you mean by "lessening the usefulness", then, yes, we *should* lessen the "usefulness" of such classes.Comment #82
ParisLiakos commentedSo, we would have to use the trait on all FormBase, ConfirmFormBase and MyConfirmFormBase?
Then i dont see the usefullness of this issue..i would rather have a protected t() method in FormBase and reuse that instead.
Comment #83
tstoecklerWell instead you would simply use the StringTranslationTrait on your own form, i.e. MyEntityDeleteForm, MyModuleConfigForm, etc. That way forms that don't need string translation won't be tainted by the methods there.
Comment #84
tim.plunkettWhat form would not need string translation?! How is that possible?
Also, "won't be tainted by the methods there" is a pretty funny thing to say about a form/controller. These are not services.
This is a long time coming, let's just finish it please.
Comment #85
Crell commentedI would rather see us dismantle the base classes in favor of letting classes piecemeal select the traits they care about. It's much more explicit that way about what their dependencies are. That said, that can be done in follow-ups and I don't see a need to make the methods private for that. I agree that would be self-defeating. So +1 on the RTBC for now.
Comment #86
catchYeah I agree with Tim in #84. It really does not bother me if forms/controllers have translation built in.
On the other hand we have real dependency issues with plugin/discovery and translation that could do with more eyes on them #2244447: Translation of low-level info/annotations leads to circular dependencies.
Patch needs a re-roll though unfortunately.
Comment #87
xanoThose that require a form POST redirect to a remote server. This is a common method that payment service providers use.
Not that this is a good reason for not adding
t()toFormBase, or anything... ;-)Comment #88
ParisLiakos commentedreroll
Comment #89
ParisLiakos commentedback
Comment #90
alexpottHmmm... methinks we need an issue summary update :)
Comment #91
ParisLiakos commentedComment #92
ParisLiakos commenteddone.
for change record I will update https://drupal.org/node/2079611 once its in
Comment #93
ParisLiakos commentedComment #94
alexpottCommitted b0821d8 and pushed to 8.x. Thanks!
Comment #96
jhodgdonThe change record update mentioned in #92 needs to take place, right?
ParisLiakos -- After you are done with that, please restore priority, status, and tags.
Comment #97
ParisLiakos commentedIts already done:)
https://drupal.org/node/2079611/revisions/view/7183121/7202909
Comment #98
jhodgdonOh sorry, thanks! I looked but obviously not carefully enough.
Comment #99
yesct commented