Commit 0e507e42 authored by alexpott's avatar alexpott

Issue #1810394 by hchonov, plach, YesCT, David Hernández, Schnitzel,...

Issue #1810394 by hchonov, plach, YesCT, David Hernández, Schnitzel, vijaycs85, penyaskito, tstoeckler, alexpott, Gábor Hojtsy: Site configuration with domain based language negotiation results in redirecting authenticated users to a different domain when accessing a content entity route for translation language different from the interface language
parent 20621c28
......@@ -21,8 +21,27 @@ interface OutboundPathProcessorInterface {
* @param string $path
* The path to process, with a leading slash.
* @param array $options
* An array of options such as would be passed to the generator's
* generateFromRoute() method.
* (optional) An associative array of additional options, with the following
* elements:
* - 'query': An array of query key/value-pairs (without any URL-encoding)
* to append to the URL.
* - 'fragment': A fragment identifier (named anchor) to append to the URL.
* Do not include the leading '#' character.
* - 'absolute': Defaults to FALSE. Whether to force the output to be an
* absolute link (beginning with http:). Useful for links that will be
* displayed outside the site, such as in an RSS feed.
* - 'language': An optional language object used to look up the alias
* for the URL. If $options['language'] is omitted, it defaults to the
* current language for the language type LanguageInterface::TYPE_URL.
* - 'https': Whether this URL should point to a secure location. If not
* defined, the current scheme is used, so the user stays on HTTP or HTTPS
* respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
* - 'base_url': Only used internally by a path processor, for example, to
* modify the base URL when a language dependent URL requires so.
* - 'prefix': Only used internally, to modify the path when a language
* dependent URL requires so.
* - 'route': The route object for the given path. It will be set by
* \Drupal\Core\Routing\UrlGenerator::generateFromRoute().
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
......
......@@ -309,6 +309,9 @@ public function generateFromRoute($name, $parameters = array(), $options = array
$name = $this->getRouteDebugMessage($name);
$this->processRoute($name, $route, $parameters, $generated_url);
$path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
// Outbound path processors might need the route object for the path, e.g.
// to get the path pattern.
$options['route'] = $route;
$path = $this->processPath($path, $options, $generated_url);
if (!empty($options['prefix'])) {
......
......@@ -29,3 +29,19 @@ function content_translation_enable() {
$message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
}
/**
* @addtogroup updates-8.0.0-rc
* @{
*/
/**
* Rebuild the routes as the content translation routes have now new names.
*/
function content_translation_update_8001() {
\Drupal::service('router.builder')->rebuild();
}
/**
* @} End of "addtogroup updates-8.0.0-rc".
*/
......@@ -53,7 +53,7 @@ function content_translation_help($route_name, RouteMatchInterface $route_match)
*/
function content_translation_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
// Move some of our hook implementations to the end of the list.
// Move our hook_entity_type_alter() implementation to the end of the list.
case 'entity_type_alter':
$group = $implementations['content_translation'];
unset($implementations['content_translation']);
......@@ -140,7 +140,11 @@ function content_translation_entity_type_alter(array &$entity_types) {
if ($entity_type->hasLinkTemplate('canonical')) {
// Provide default route names for the translation paths.
if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
$entity_type->setLinkTemplate('drupal:content-translation-overview', $entity_type->getLinkTemplate('canonical') . '/translations');
$translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
$entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
$entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
$entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
$entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
}
// @todo Remove this as soon as menu access checks rely on the
// controller. See https://www.drupal.org/node/2155787.
......
......@@ -652,7 +652,7 @@ public function entityFormSourceChange($form, FormStateInterface $form_state) {
$source = $form_state->getValue(array('source_langcode', 'source'));
$entity_type_id = $entity->getEntityTypeId();
$form_state->setRedirect('content_translation.translation_add_' . $entity_type_id, array(
$form_state->setRedirect("entity.$entity_type_id.content_translation_add", array(
$entity_type_id => $entity->id(),
'source' => $source,
'target' => $form_object->getFormLangcode($form_state),
......@@ -689,7 +689,7 @@ function entityFormDeleteTranslation($form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($entity->urlInfo('delete-form'));
}
else {
$form_state->setRedirect('content_translation.translation_delete_' . $entity_type_id, [
$form_state->setRedirect("entity.$entity_type_id.content_translation_delete", [
$entity_type_id => $entity->id(),
'language' => $form_object->getFormLangcode($form_state),
]);
......
......@@ -127,7 +127,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
$langcode = $language->getId();
$add_url = new Url(
'content_translation.translation_add_' . $entity_type_id,
"entity.$entity_type_id.content_translation_add",
array(
'source' => $original,
'target' => $language->getId(),
......@@ -138,7 +138,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
)
);
$edit_url = new Url(
'content_translation.translation_edit_' . $entity_type_id,
"entity.$entity_type_id.content_translation_edit",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
......@@ -148,7 +148,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL
)
);
$delete_url = new Url(
'content_translation.translation_delete_' . $entity_type_id,
"entity.$entity_type_id.content_translation_delete",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
......
......@@ -112,7 +112,7 @@ protected function alterRoutes(RouteCollection $collection) {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_add_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_add", $route);
$route = new Route(
$path . '/edit/{language}',
......@@ -137,7 +137,7 @@ protected function alterRoutes(RouteCollection $collection) {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_edit_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_edit", $route);
$route = new Route(
$path . '/delete/{language}',
......@@ -162,7 +162,7 @@ protected function alterRoutes(RouteCollection $collection) {
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_delete_$entity_type_id", $route);
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
}
}
......
......@@ -107,7 +107,8 @@ protected function doTestBasicTranslation() {
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = $this->getNewEntityValues($langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
......@@ -167,7 +168,8 @@ protected function doTestBasicTranslation() {
$language = ConfigurableLanguage::load($langcode);
$source_langcode = 'it';
$edit = array('source_langcode[source]' => $source_langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
......@@ -180,7 +182,8 @@ protected function doTestBasicTranslation() {
// Add another translation and mark the other ones as outdated.
$values[$langcode] = $this->getNewEntityValues($langcode);
$edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
......@@ -207,13 +210,15 @@ protected function doTestBasicTranslation() {
*/
protected function doTestTranslationOverview() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
$translate_url = $entity->urlInfo('drupal:content-translation-overview');
$this->drupalGet($translate_url);
$translate_url->setAbsolute(FALSE);
foreach ($this->langcodes as $langcode) {
if ($entity->hasTranslation($langcode)) {
$language = new Language(array('id' => $langcode));
$view_path = $entity->url('canonical', array('language' => $language));
$elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path));
$view_url = $entity->url('canonical', ['language' => $language]);
$elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]);
$this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode)));
$edit_path = $entity->url('edit-form', array('language' => $language));
$elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path));
......@@ -343,7 +348,7 @@ protected function doTestTranslationDeletion() {
// Check that the translator cannot delete the original translation.
$args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
$this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args));
$this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
$this->assertResponse(403);
}
......
......@@ -73,7 +73,7 @@ protected function setupEntity() {
// Create a translation.
$this->drupalLogin($this->translator);
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$this->drupalPostForm($add_translation_url, array(), t('Save'));
$this->rebuildContainer();
}
......@@ -175,7 +175,7 @@ protected function doTestWorkflows(UserInterface $user, $expected_status) {
$this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args));
// Check whether the user is allowed to create a translation.
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
if ($expected_status['add_translation'] == 200) {
$this->clickLink('Add');
$this->assertUrl($add_translation_url->toString(), [], 'The translation overview points to the translation form when creating translations.');
......@@ -193,7 +193,7 @@ protected function doTestWorkflows(UserInterface $user, $expected_status) {
// Check whether the user is allowed to edit a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$edit_translation_url = Url::fromRoute('content_translation.translation_edit_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
$edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['edit_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['edit'] == 200;
......@@ -221,7 +221,7 @@ protected function doTestWorkflows(UserInterface $user, $expected_status) {
// Check whether the user is allowed to delete a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$delete_translation_url = Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
$delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['delete_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['delete'] == 200;
......
<?php
/**
* @file
* Update functions for Language module.
*/
/**
* Rebuild the container as services changed.
*/
function language_update_8001() {
\Drupal::service('kernel')->invalidateContainer();
}
......@@ -9,7 +9,7 @@ services:
- [initLanguageManager]
language.config_subscriber:
class: Drupal\language\EventSubscriber\ConfigSubscriber
arguments: ['@language_manager', '@language.default', '@config.factory']
arguments: ['@language_manager', '@language.default', '@config.factory', '@language_negotiator']
tags:
- { name: event_subscriber }
language.config_factory_override:
......
......@@ -14,6 +14,8 @@
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\language\ConfigurableLanguageManager;
use Drupal\language\HttpKernel\PathProcessorLanguage;
use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -42,6 +44,20 @@ class ConfigSubscriber implements EventSubscriberInterface {
*/
protected $configFactory;
/**
* The language negotiator.
*
* @var \Drupal\language\LanguageNegotiatorInterface
*/
protected $languageNegotiator;
/**
* The language path processor.
*
* @var \Drupal\language\HttpKernel\PathProcessorLanguage
*/
protected $pathProcessorLanguage;
/**
* Constructs a new class object.
*
......@@ -51,11 +67,14 @@ class ConfigSubscriber implements EventSubscriberInterface {
* The default language.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\language\LanguageNegotiatorInterface $language_negotiator
* The language negotiator.
*/
public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory) {
public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory, LanguageNegotiatorInterface $language_negotiator) {
$this->languageManager = $language_manager;
$this->languageDefault = $language_default;
$this->configFactory = $config_factory;
$this->languageNegotiator = $language_negotiator;
}
/**
......@@ -102,6 +121,25 @@ public function onConfigSave(ConfigCrudEvent $event) {
// Trigger a container rebuild on the next request by invalidating it.
ConfigurableLanguageManager::rebuildServices();
}
elseif ($saved_config->getName() == 'language.types' && $event->isChanged('negotiation')) {
// If the negotiation configuration changed the language negotiator and
// the language path processor have to be reset so that they regenerate
// the method instances and also sort them accordingly to the new config.
$this->languageNegotiator->reset();
if (isset($this->pathProcessorLanguage)) {
$this->pathProcessorLanguage->reset();
}
}
}
/**
* Injects the language path processors on multilingual site configuration.
*
* @param \Drupal\language\HttpKernel\PathProcessorLanguage $path_processor_language
* The language path processor.
*/
public function setPathProcessorLanguage(PathProcessorLanguage $path_processor_language) {
$this->pathProcessorLanguage = $path_processor_language;
}
/**
......
......@@ -13,6 +13,7 @@
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\language\EventSubscriber\ConfigSubscriber;
use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Session\AccountInterface;
......@@ -57,6 +58,14 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
*/
protected $multilingual;
/**
* The language configuration event subscriber.
*
* @var \Drupal\language\EventSubscriber\ConfigSubscriber
*/
protected $configSubscriber;
/**
* Constructs a PathProcessorLanguage object.
*
......@@ -68,12 +77,15 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
* The language negotiator.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current active user.
* @param \Drupal\language\EventSubscriber\ConfigSubscriber $config_subscriber
* The language configuration event subscriber.
*/
public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user) {
public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user, ConfigSubscriber $config_subscriber) {
$this->config = $config;
$this->languageManager = $language_manager;
$this->negotiator = $negotiator;
$this->negotiator->setCurrentUser($current_user);
$this->configSubscriber = $config_subscriber;
}
/**
......@@ -152,4 +164,22 @@ protected function initProcessors($scope) {
});
}
/**
* Initializes the injected event subscriber with the language path processor.
*
* The language path processor service is registered only on multilingual
* site configuration, thus we inject it in the event subscriber only when
* it is initialized.
*/
public function initConfigSubscriber() {
$this->configSubscriber->setPathProcessorLanguage($this);
}
/**
* Resets the collected processors instances.
*/
public function reset() {
$this->processors = array();
}
}
......@@ -39,7 +39,9 @@ public function register(ContainerBuilder $container) {
->addArgument(new Reference('config.factory'))
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('language_negotiator'))
->addArgument(new Reference('current_user'));
->addArgument(new Reference('current_user'))
->addArgument(new Reference('language.config_subscriber'))
->addMethodCall('initConfigSubscriber');
}
}
......
<?php
/**
* @file
* Contains \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity.
*/
namespace Drupal\language\Plugin\LanguageNegotiation;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Url;
use Drupal\language\LanguageNegotiationMethodBase;
use Drupal\language\LanguageSwitcherInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Class for identifying the content translation language.
*
* @LanguageNegotiation(
* id = Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::METHOD_ID,
* types = {Drupal\Core\Language\LanguageInterface::TYPE_CONTENT},
* weight = -9,
* name = @Translation("Content language"),
* description = @Translation("Determines the content language from a request parameter."),
* )
*/
class LanguageNegotiationContentEntity extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface, ContainerFactoryPluginInterface {
/**
* The language negotiation method ID.
*/
const METHOD_ID = 'language-content-entity';
/**
* The query string parameter.
*/
const QUERY_PARAMETER = 'language_content_entity';
/**
* A list of all the link paths of enabled content entities.
*
* @var array
*/
protected $contentEntityPaths;
/**
* Static cache for the language negotiation order check.
*
* @see \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::hasLowerLanguageNegotiationWeight()
*
* @var bool
*/
protected $hasLowerLanguageNegotiationWeightResult;
/**
* Static cache of outbound route paths per request.
*
* @var \SplObjectStorage
*/
protected $paths;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new LanguageNegotiationContentEntity instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
$this->paths = new \SplObjectStorage();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($container->get('entity.manager'));
}
/**
* {@inheritdoc}
*/
public function getLangcode(Request $request = NULL) {
$langcode = $request->get(static::QUERY_PARAMETER);
$language_enabled = array_key_exists($langcode, $this->languageManager->getLanguages());
return $language_enabled ? $langcode : NULL;
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
// If appropriate, process outbound to add a query parameter to the url and
// remove the language option, so that url negotiator does not rewrite the
// url.
// First, check if processing conditions are met.
if (!($request && !empty($options['route']) && $this->hasLowerLanguageNegotiationWeight() && $this->meetsContentEntityRoutesCondition($options['route'], $request))) {
return $path;
}
if (isset($options['language']) || $langcode = $this->getLangcode($request)) {
// If the language option is set, unset it, so that the url language
// negotiator does not rewrite the url.
if (isset($options['language'])) {
$langcode = $options['language']->getId();
unset($options['language']);
}
if (isset($options['query']) && is_string($options['query'])) {
$query = [];
parse_str($options['query'], $query);
$options['query'] = $query;
}
else {
$options['query'] = [];
}
if (!isset($options['query'][static::QUERY_PARAMETER])) {
$query_addon = [static::QUERY_PARAMETER => $langcode];
$options['query'] += $query_addon;
// @todo Remove this once https://www.drupal.org/node/2507005 lands.
$path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($query_addon);
}
if ($bubbleable_metadata) {
// Cached URLs that have been processed by this outbound path
// processor must be:
$bubbleable_metadata
// - varied by the content language query parameter.
->addCacheContexts(['url.query_args:' . static::QUERY_PARAMETER]);
}
}
return $path;
}
/**
* {@inheritdoc}
*/
public function getLanguageSwitchLinks(Request $request, $type, Url $url) {
$links = [];
$query = [];
parse_str($request->getQueryString(), $query);
foreach ($this->languageManager->getNativeLanguages() as $language) {
$langcode = $language->getId();
$query[static::QUERY_PARAMETER] = $langcode;
$links[$langcode] = [
'url' => $url,
'title' => $language->getName(),
'attributes' => ['class' => ['language-link']],
'query' => $query,
];
}
return $links;
}
/**
* Determines if content entity language negotiator has higher priority.
*
* The content entity language negotiator having higher priority than the url
* language negotiator, is a criteria in
* \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::processOutbound().
*