Skip to content

[Translation] Remove TranslatorBagInterface to allow for optimized caching in 3.0 #14530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed

[Translation] Remove TranslatorBagInterface to allow for optimized caching in 3.0 #14530

wants to merge 14 commits into from

Conversation

mpdude
Copy link
Contributor

@mpdude mpdude commented May 3, 2015

Q A
Bug fix? no
New feature? no
BC breaks? no
Deprecations? yes
Tests pass? yes
Fixed tickets
License MIT
Doc PR n/a

We would like the Translator to be able to perform various optimizations on the MessageCatalogues it holds internally. One particular example is working with reduced catalogues for fallback locales that only contain the messages not present in one of the more preferred locales, or possibly do a lazy-loading of catalogues for fallback locales.

These optimizations are not possible as long as clients are able to retrieve the MessageCatalogue instances used inside the Translator by means of the TranslatorBagInterface and expect them to be full-fledged and complete catalogue instances (so the initial attempt had to be reverted in #14315, also see #14380 for a discussion).

In other words, TranslatorBagInterface breaks a boundary of encapsulation important for optimizations.

The two clients of the interface inside Symfony are not really interested in the catalogues per se, but only need them to iterate along the fallback chain in order to figure out the locale ultimately used. This information can now be obtained through the new FallbackLocaleAwareInterface.

To be discussed:

  • In 3.0, should FallbackLocaleAwareInterface remain a dedicated interface or become part of TranslatorInterface?
  • Shall we keep the TranslatorBagInterface in the future, especially if Translator does not implement it?

@@ -506,4 +522,19 @@ private function getConfigCacheFactory()

return $this->configCacheFactory;
}

private function getCatalogueInternal($locale = null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could rename this back to getCatalogue() and make it private once we do no longer implement TranslatorBagInterface (in 3.0).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add this note above.

@@ -213,54 +213,6 @@ public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales(
$this->assertEquals('bar', $translator->trans('bar'));
}

public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardlessOfCaching()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it shoud be removed in 3.0 rename it into testLegacyPrimaryA.. instead and add

$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);

*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface FallbackLocaleAwareInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TranslatorLocaleAwareInterface ?

@@ -35,8 +35,8 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
*/
public function __construct(TranslatorInterface $translator)
{
if (!$translator instanceof TranslatorBagInterface) {
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
if (!($translator instanceof TranslatorBagInterface && $translator instanceof FallbackLocaleAwareInterface)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requiring it to implement both is actually a BC break as not implementing FallbackLocaleAwareInterface was allowed previously. You are restricting the supported argument, and the BC promise only allows to widen it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the right solution would be to provide a Translator implementation which would implement FallbackLocaleAwareInterface on top of TranslatorBagInterface and wrap the translator into this implementation when it implements TranslatorBagInterface but not FallbackLocaleAwareInterface. This would make things BC, avoid duplication between the DataCollectorTranslator and the LoggingTranslator and not have performance penalty for the normal case (given that the default translator implements the new interface)

@stof
Copy link
Member

stof commented May 4, 2015

@mpdude I just thought about another case where TranslatorBagInterface could be useful: https://github.com/willdurand/BazingaJsTranslationBundle/ is exposing translation catalogues to the JS code. Currently, it does that by reimplementing the loading itself (it gets all translation loaders from Symfony and gets all resources as well, all of which is done by the Translator) because TranslatorBagInterface was not available at that time. What would be the clean way toimplement such your case in your new architecture (duplicating the loading is not the clean way in the 2.6 architecture IMO, but it was probably the only way in the 2.3 architecture, which is why it is used)

@aitboudad
Copy link
Contributor

well I think we need to keep getCatalogue and the optimize caching should be done by optimizing MessageCatalogue see #14526

@mpdude
Copy link
Contributor Author

mpdude commented May 4, 2015

@stof In the case of messages available in both the primary and in fallback catalogues, which set of messages do you think this bundle would need?

The problem is that if we burden Translator with general tasks like providing complete MessageCatalogue chains, it gets harder to optimize the data structures used internally for the "find best translation available" use case (the trans() and transChoice() methods).

We could, of course, internally use optimized structures for these methods plus a "complete" set of MessageCatalogue chains for the TranslatorBagInterface-based use cases, but that would clearly be a nightmare to maintain and would not make a good example for the SRP.

So, what if we had two different classes (or services in the case of the full-stack framework):

The first one is where the resources and loaders are registered. You tell it your preferred order of locales, it will return a full-fledged MessageCatalogue chain (expensive operation!).

The second one is the Translator. It will initially obtain the MessageCatalogue from the first class, but then optimize/tweak things as it sees fit for the trans() and transChoice() methods. These optimized structures can be kept in the cache and will not be exposed to the outside world.

@stof
Copy link
Member

stof commented May 4, 2015

@mpdude This bundle would need anything needed to be able to reimplement trans and transChoice in JS. So it would need the full catalogues for any active locales (it currently supports activating multiple locales, in case the locale choice need to be done on the JS side, or to keep a single translation file for all locales to simplify the loading of the JS), but only fallback messages might be enough for other locales.

And I agree that delegating all the loading to a separate class might be a better architecture. It could also simplify a bunch of things:

  • lazy-loading of loaders could be implemented by implementing this interface in a lazy way (or relying of the DI feature to build the lazy implementation) instead of extending the Translator in FrameworkBundle, which would also make the lazy-loading available for people using DI+Translator outside Symfony
  • caching of catalogues could be implemented by composition instead of being in the main class dealing with loaders (which does not forbid caching the internal structures of the Translator class, which is a separate topic).

This is something I already thought about in the past. The hard part is the Translator class is currently designed to be extendable through inheritance. Extracting this logic into separate collaborators in a BC way will likely be very hard.

@aitboudad
Copy link
Contributor

@stof FYI I work on delegating all the loading :)

@stof
Copy link
Member

stof commented May 4, 2015

@aitboudad have you found a way to keep BC for people overwriting protected methods of the Translator to customize the Translator through its current extension points ?

@aitboudad
Copy link
Contributor

@stof see an initial work for DelegationLoader I'll complete it soon, I think we need to deprecate Symfony\Bundle\FrameworkBundle\Translation\Translator.
To avoid BC we will create a new translator service and deprecate the default one.

@aitboudad
Copy link
Contributor

@mpdude why we can't probably not move that responsibility out into another class ? until now
I have only one issue about initializeCatalogue, I'll see how to deal with it.

@stof should we move handling fallback catalogues into DelegationLoader or by composition instead ?

@fabpot
Copy link
Member

fabpot commented Sep 24, 2015

What is the status of this PR? ping @aitboudad

@mpdude
Copy link
Contributor Author

mpdude commented Sep 24, 2015

I rebased this branch and fixed the various issues from the first review by @aitboudad and @stof.

This won't take us to the ultimate goal of a better and more efficient Translator (#13948) because we'd probably also need to factor loading out of the Translator and prohibit inheritance (this and that comment and maybe something like the #14622 or #14671 PRs?)

Anyway, this IMO improves our situation because we can (in 3.0) remove an interface that definetly is a show-stopper.

@@ -39,6 +44,12 @@ public function __construct(TranslatorInterface $translator)
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to fix that. The interface is deprecated and must not be required.

@aitboudad
Copy link
Contributor

@fabpot ll try to finish #14622 today as I think it's more efficient than this one(ideally we should have something like https://github.com/openl10n/openl10n-translation/tree/master/src/Openl10n/Component/Translation/MessageCatalogue)

@mpdude mpdude changed the title [RFC] [Translation] Remove TranslatorBagInterface to allow for optimized caching in 3.0 [Translation] Remove TranslatorBagInterface to allow for optimized caching in 3.0 Oct 3, 2015
@mpdude
Copy link
Contributor Author

mpdude commented Oct 3, 2015

@symfony/deciders I would like to ask for your vote on this.

I believe that removing the TranslatorBagInterface is one important prerequisite for more efficient caching and/or loading inside the Translator because it improves encapsulation.

With the current state of this PR, one important thing to note is that we leave clients without a way to retrieve the message catalogue from the translator, a feature added in 2.6 "by accident" (it would not have been necessary for the feature we added back then).

To provide an alternate means of retrieving the catalogue is an on-going effort in #14622 and #14671, but with the internal structure of Translator (different code paths, mixing several concerns, protected methods and @api) I am unsure if we can make it.

So, the decision you need to make is whether the improvement this PR brings is worth this possible annoyance for users of the TranslatorBagInterface in case that no other replacement for it can be provided.

Note that @stof refers to one third-party bundle that works without this interface by implementing the loading itself.

(The failing test is for PHP 7 is completely unrelated.)

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Dec 6, 2016
@nicolas-grekas
Copy link
Member

I think we don't need this one: optimized caching is already dealt with on PHP7, where static arrays and static strings are far superior optimizing strategies. Thus, no need to do anything more on our side to me.

@jakzal
Copy link
Contributor

jakzal commented Feb 28, 2017

@mpdude any thoughts on what Nicolas said?

@nicolas-grekas
Copy link
Member

Closing as explained. Of course, please reopen if you don't agree.

@mpdude
Copy link
Contributor Author

mpdude commented Jul 12, 2017

I opened this a while ago when I was working on a project where we had issues due to a large number of languages supported and comprehensive catalogs.

We found other ways to work around this and I cannot tell whether the problem would still appear on PHP7.

So, closing this is fine for me. Will reopen it or make a new case should I run into it again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants