Sort out and fix language fallback inconsistencies

Created on 8 March 2018, over 6 years ago
Updated 18 September 2023, 10 months ago

Problem/Motivation

The D8 builtin language system was a huge step forward and adding a fallback system to core an important appreciation that we need something like fallbacks. There are some reports about inconsistencies, which at first sound quite different, but may be foundation for improving / fixing the API. Here's a well-shuffled list:

  • EntityRepository::getTranslationFromContext returns the passed-in entity translationif it does not find a fallback. This makes "Entity does not have a fallback" indistinguishable from "Entity fallback is the passed-in entity translation.
  • Also: In the absence of a fallback, the returned fallback is the passed-in entity translation (whach may be random).
  • The above leads to the fact that results depend which of 2 translations is the original and which the translation. Which is at least highly counter-intuitive.
  • The above makes it impossible to "distiguish between "current translation is a fallback $langcode" and "$langcode does not have a fallback"" (axel.rutz #16)
  • The above leads to the fact that adding an unpublished translation makes that language go from "fallback to original" to "access denied" (which is at least counterintuitive).
  • "content_translation removes fallbacks for entities that the current user does not have access to. But access [should not] be mixed with fallbacks" It torpedoes e.g. getting fallbacks for search_api indexing. (axel.rutz #18)
  • #2972308: Allow users to translate content they can edit β†’ "shows that accessing an entity through an entity reference field vs. its route has a completely different behavior, that again differs based on whether the entity has its own status that it checks too vs. just a translation status."(@berdir #2978048-10: Unpublished translations should fallback to the original language β†’ )
  • πŸ› ProcessedText filter doesn't pass in correct language when using language fallback Needs work
  • Last and minor: EntityRepository::getTranslationFromContext assumes the first fallback of a language is the language itself. While there may or may not be use cases for dropping that assumption, if we lay hands on this anyway, there's no reason to hinder the flexibility of dropping that assumption.

Some exegesis on how this came into being and how to proceed:

  • "the main goal [of the fallback system] was to keep the navigation structure intact for highly "symmetric" multilingual sites". (@plach #9) As of the above, implementing "asymmetric" sites where some languages do not have a fallback currently is not possible. Fixing is not easy "since the language fallback API does not support returning no candidates"(@plach #9)
  • "I spoke with Gabor about this and he reminded that at the time we introduced this functionality we were planning to let contrib take care of all the possible fallback alternative behaviors. Of course we are both fine with performing any core change needed to enable this to happen in contrib." (@plach #13)

Proposed resolution

The big picture:

  • Provide a setting and upgrade path that makes legacy sites behave as they did. So the following only applies to sites that are new or actively opt in.
  • Make all fallbacks explicit. Translations without fallback return 403 (@plach #9, satisfies @sdewitt #8)
  • Add the option to "let everything fallback to original translation" (@plach #9, satisfies @berdir #7)
  • To add that option, we can simply add a fallback UI to core. The code is rather trivial which led to the unfortunate sutiation that we currently have at least 3 contrib modules for this. See #2552663: Consider deprecating language_fallback in favor of entity_language_fallback β†’ . This should satisfy >90% of all fallback use cases, everything more complex can easily be done via a custom hook implementation.

Concrete steps:

(TBD)

Remaining tasks

* Sort out the problem(s)
* Create consensus on how to proceed with it
** Especially: What is the semantics of fallback and how does it relate to entity access)
** And: Do we need separate "entity_view" and "entity_upcast" operations? [#]
* Do it

User interface changes

None.

API changes

TBD

Data model changes

None.

Release notes snippet

TBD

Original report

Problem/Motivation

DESCRIPTION

Moderated content created in a source language using the default Editorial workflow is accessible live using all language prefixes as soon as it is published and prior to actually adding any translations for the node.

REPRODUCTION STEPS

  • Install Drupal 8.5
  • Enable content translation
  • Enable content moderation
  • Install drush via composer
  • Run drush cr
  • Login to Drupal
  • Navigate to /admin/config/regional/language
  • Add French and German languages
  • Navigate to /admin/config/regional/content-language
  • Enable "Content" for translation under "Custom language settings"
  • Enable "Article" for translation under "Content"
  • Navigate to /admin/config/workflow/workflows
  • Edit the "Editorial" workflow and apply it to the "Article" content type
  • Navigate to /admin/structure/types/manage/article
  • Under "Publishing options", uncheck "Published" and "Promote to front page" and save
  • Navigate to /admin/content
  • Create a new Article node in "Draft" workflow state
  • Use the moderation state widget to move the new node from Draft to Published
  • In an incognito window or another browser, attempt to hit /node/nid (e.g. /node/5) and notice that it shows the expected published content.
  • In the same window, change the URL to /fr/node/nid (e.g. /fr/node/5) and notice that it shows the published English (or source language) content!
  • Try /de/node/nid and notice it also shows the published English (or source language) content
  • Try using a language code/prefix that is not registered with Drupal yet, e.g. Arabic, /ar/node/nid and notice you get a 404 Not Found

EXPECTED RESULT

No content should be available at a language prefix URL until a default published revision of the content exists with that language code/prefix.

ACTUAL RESULT

The default published revision of the source language content is available under all registered language prefixes.

Preliminary Analysis

My cursory analysis of this problem suggests this behavior occurs due to the following sequence of events.

  • A drupal request enters the kernel for /fr/node/nid
  • The HttpKernel fires the REQUEST event
  • The ContainerAwareEventDispatcher invokes appropriate callables for the REQUEST event
  • The Symfony RouterListener invokes the AccessAwareRouter to match the request
  • The Drupal routers getInitialRouteCollection() is invoked to collect candidate routes
  • The Drupal route providers getRouteCollectionForRequest() is invoked to build the candidate routes
  • Drupal's path processor system is run to process the inbound path
  • The PathProcessorLanguage uses the LanguageNegotiationUrl to parse the language prefix and it rebuilds the inbound path without the prefix
  • Candidate route definitions are determined by getRouteByPath()
  • The entity.node.canonical route is chosen for the request
  • The route enhancers are invoked
  • The ParamConversionEnhancer is invoked to resolve/convert inbound parameters
  • The ParamConverterManager invokes converters for all matching parameters
  • The EntityRevisionConverter is invoked which defers to its parent EntityConverter for routes not loading the latest revision
  • The EntityConverter converts the node parameter from the URL pattern for the entity.node.canonical route definition to an actual Node instance. It is important to note that at this step, the EntityConverter loads the default revision from the node storage. In our example above, this means that it loads the published revision of the node nid. It does not yet take into account the langcode.
  • Next, the EntityConverter invokes the EntityRespository::getTranslationFromContext() method to potentially replace the default published entity instance with a translation instance. The critical step causing the issue happens next.
  • The getTranslationFromContext() method performs the following check:

    if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {

    At this point, the default published revision of $entity does not have any translation languages so it acts as an identity transform and returns $entity unchanged. This is probably the correct and desired behavior in many situations where this method is called (which I believe is in many different places). It does not seem like a great behavior in this situation since the calling code has no idea that no translation was found for the requested entity + current language.

  • The default published revision of $entity is returned from the EntityConverter and the node parameter is replaced by the $entity instance for further processing down the line.
  • The now filtered, and enhanced route definition is returned to the router and the access is checked. At this point, since we have the default published $entity, access checks pass, and control is returned to the ContainerAwareEventDispatcher which subsequently invokes the rendering engine to render the page.

Proposed resolution

A few possible solutions might include the following, the impact of which would have to be carefully considered for existing behavior and BC.

  1. Add a flag to the getTranslationFromContext() method that when set to TRUE (defaults to FALSE to maintain BC), returns NULL if no translation is found for the current language context.
  2. Update the EntityConverter to check if the $entity returned from getTranslationFromContext() has a language code that matches the current language or any of it's fallbacks and if not, throw a ResourceNotFoundException
  3. Add a RouteEnhancer that performs some similar check as above.
  4. Could we use route requirements somehow so that handleRouteRequirements would report a mismatch?

Remaining tasks

User interface changes

API changes

Data model changes

πŸ› Bug report
Status

Needs work

Version

11.0 πŸ”₯

Component
Language systemΒ  β†’

Last updated 3 days ago

  • Maintained by
  • πŸ‡©πŸ‡ͺGermany @sun
Created by

πŸ‡ΊπŸ‡ΈUnited States sdewitt

Live updates comments and jobs are added and updated live.
  • Needs issue summary update

    Issue summaries save everyone time if they are kept up-to-date. See Update issue summary task instructions.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.69.0 2024