--- /dev/null
+<?php
+
+namespace Drupal\language;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Site\Settings;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Class responsible for performing language negotiation.
+ */
+class LanguageNegotiator implements LanguageNegotiatorInterface {
+
+ /**
+ * The language negotiation method plugin manager.
+ *
+ * @var \Drupal\Component\Plugin\PluginManagerInterface
+ */
+ protected $negotiatorManager;
+
+ /**
+ * The language manager.
+ *
+ * @var \Drupal\language\ConfigurableLanguageManagerInterface
+ */
+ protected $languageManager;
+
+ /**
+ * The configuration factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
+ */
+ protected $configFactory;
+
+ /**
+ * The settings instance.
+ *
+ * @return \Drupal\Core\Site\Settings
+ */
+ protected $settings;
+
+ /**
+ * The request stack object.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * The current active user.
+ *
+ * @return \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * Local cache for language negotiation method instances.
+ *
+ * @var array
+ */
+ protected $methods;
+
+ /**
+ * An array of language objects keyed by method id.
+ *
+ * @var \Drupal\Core\Language\LanguageInterface[]
+ */
+ protected $negotiatedLanguages = [];
+
+ /**
+ * Constructs a new LanguageNegotiator object.
+ *
+ * @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
+ * The language manager.
+ * @param \Drupal\Component\Plugin\PluginManagerInterface $negotiator_manager
+ * The language negotiation methods plugin manager
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * The configuration factory.
+ * @param \Drupal\Core\Site\Settings $settings
+ * The settings instance.
+ */
+ public function __construct(ConfigurableLanguageManagerInterface $language_manager, PluginManagerInterface $negotiator_manager, ConfigFactoryInterface $config_factory, Settings $settings, RequestStack $requestStack) {
+ $this->languageManager = $language_manager;
+ $this->negotiatorManager = $negotiator_manager;
+ $this->configFactory = $config_factory;
+ $this->settings = $settings;
+ $this->requestStack = $requestStack;
+ }
+
+ /**
+ * Initializes the injected language manager with the negotiator.
+ *
+ * This should be called right after instantiating the negotiator to make it
+ * available to the language manager without introducing a circular
+ * dependency.
+ */
+ public function initLanguageManager() {
+ $this->languageManager->setNegotiator($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reset() {
+ $this->negotiatedLanguages = [];
+ $this->methods = [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCurrentUser(AccountInterface $current_user) {
+ $this->currentUser = $current_user;
+ $this->reset();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initializeType($type) {
+ $language = NULL;
+
+ if ($this->currentUser) {
+ // Execute the language negotiation methods in the order they were set up
+ // and return the first valid language found.
+ foreach ($this->getEnabledNegotiators($type) as $method_id => $info) {
+ if (!isset($this->negotiatedLanguages[$method_id])) {
+ $this->negotiatedLanguages[$method_id] = $this->negotiateLanguage($type, $method_id);
+ }
+
+ // Since objects are references, we need to return a clone to prevent
+ // the language negotiation method cache from being unintentionally
+ // altered. The same methods might be used with different language types
+ // based on configuration.
+ $language = !empty($this->negotiatedLanguages[$method_id]) ? clone($this->negotiatedLanguages[$method_id]) : NULL;
+
+ if ($language) {
+ $this->getNegotiationMethodInstance($method_id)->persist($language);
+ break;
+ }
+ }
+ }
+
+ if (!$language) {
+ // If no other language was found use the default one.
+ $language = $this->languageManager->getDefaultLanguage();
+ $method_id = static::METHOD_ID;
+ }
+
+ return [$method_id => $language];
+ }
+
+ /**
+ * Gets enabled detection methods for the provided language type.
+ *
+ * @param string $type
+ * The language type.
+ *
+ * @return array
+ * An array of enabled detection methods for the provided language type.
+ */
+ protected function getEnabledNegotiators($type) {
+ return $this->configFactory->get('language.types')->get('negotiation.' . $type . '.enabled') ?: [];
+ }
+
+ /**
+ * Performs language negotiation using the specified negotiation method.
+ *
+ * @param string $type
+ * The language type to be initialized.
+ * @param string $method_id
+ * The string identifier of the language negotiation method to use to detect
+ * language.
+ *
+ * @return \Drupal\Core\Language\LanguageInterface|null
+ * Negotiated language object for given type and method, FALSE otherwise.
+ */
+ protected function negotiateLanguage($type, $method_id) {
+ $langcode = NULL;
+ $method = $this->negotiatorManager->getDefinition($method_id);
+
+ if (!isset($method['types']) || in_array($type, $method['types'])) {
+ $langcode = $this->getNegotiationMethodInstance($method_id)->getLangcode($this->requestStack->getCurrentRequest());
+ }
+
+ $languages = $this->languageManager->getLanguages();
+ return isset($languages[$langcode]) ? $languages[$langcode] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNegotiationMethods($type = NULL) {
+ $definitions = $this->negotiatorManager->getDefinitions();
+ if (isset($type)) {
+ $enabled_methods = $this->getEnabledNegotiators($type);
+ $definitions = array_intersect_key($definitions, $enabled_methods);
+ }
+ return $definitions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNegotiationMethodInstance($method_id) {
+ if (!isset($this->methods[$method_id])) {
+ $instance = $this->negotiatorManager->createInstance($method_id, []);
+ $instance->setLanguageManager($this->languageManager);
+ $instance->setConfig($this->configFactory);
+ $instance->setCurrentUser($this->currentUser);
+ $this->methods[$method_id] = $instance;
+ }
+ return $this->methods[$method_id];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPrimaryNegotiationMethod($type) {
+ $enabled_methods = $this->getEnabledNegotiators($type);
+ return empty($enabled_methods) ? LanguageNegotiatorInterface::METHOD_ID : key($enabled_methods);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isNegotiationMethodEnabled($method_id, $type = NULL) {
+ $enabled = FALSE;
+ $language_types = !empty($type) ? [$type] : $this->languageManager->getLanguageTypes();
+
+ foreach ($language_types as $type) {
+ $enabled_methods = $this->getEnabledNegotiators($type);
+ if (isset($enabled_methods[$method_id])) {
+ $enabled = TRUE;
+ break;
+ }
+ }
+
+ return $enabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveConfiguration($type, $enabled_methods) {
+ // As configurable language types might have changed, we reset the cache.
+ $this->languageManager->reset();
+ $definitions = $this->getNegotiationMethods();
+ $default_types = $this->languageManager->getLanguageTypes();
+
+ // Order the language negotiation method list by weight.
+ asort($enabled_methods);
+ foreach ($enabled_methods as $method_id => $weight) {
+ if (isset($definitions[$method_id])) {
+ $method = $definitions[$method_id];
+ // If the language negotiation method does not express any preference
+ // about types, make it available for any configurable type.
+ $types = array_flip(!empty($method['types']) ? $method['types'] : $default_types);
+ // Check whether the method is defined and has the right type.
+ if (!isset($types[$type])) {
+ unset($enabled_methods[$method_id]);
+ }
+ }
+ else {
+ unset($enabled_methods[$method_id]);
+ }
+ }
+ $this->configFactory->getEditable('language.types')->set('negotiation.' . $type . '.enabled', $enabled_methods)->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function purgeConfiguration() {
+ // Ensure that we are getting the defined language negotiation information.
+ // An invocation of \Drupal\Core\Extension\ModuleInstaller::install() or
+ // \Drupal\Core\Extension\ModuleInstaller::uninstall() could invalidate the
+ // cached information.
+ $this->negotiatorManager->clearCachedDefinitions();
+ $this->languageManager->reset();
+ foreach ($this->languageManager->getDefinedLanguageTypesInfo() as $type => $info) {
+ $this->saveConfiguration($type, $this->getEnabledNegotiators($type));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateConfiguration(array $types) {
+ // Ensure that we are getting the defined language negotiation information.
+ // An invocation of \Drupal\Core\Extension\ModuleInstaller::install() or
+ // \Drupal\Core\Extension\ModuleInstaller::uninstall() could invalidate the
+ // cached information.
+ $this->negotiatorManager->clearCachedDefinitions();
+ $this->languageManager->reset();
+
+ $language_types = [];
+ $language_types_info = $this->languageManager->getDefinedLanguageTypesInfo();
+ $method_definitions = $this->getNegotiationMethods();
+
+ foreach ($language_types_info as $type => $info) {
+ $configurable = in_array($type, $types);
+
+ // The default language negotiation settings, if available, are stored in
+ // $info['fixed'].
+ $has_default_settings = !empty($info['fixed']);
+ // Check whether the language type is unlocked. Only the status of
+ // unlocked language types can be toggled between configurable and
+ // non-configurable.
+ if (empty($info['locked'])) {
+ if (!$configurable && !$has_default_settings) {
+ // If we have an unlocked non-configurable language type without
+ // default language negotiation settings, we use the values
+ // negotiated for the interface language which, should always be
+ // available.
+ $method_weights = [LanguageNegotiationUI::METHOD_ID];
+ $method_weights = array_flip($method_weights);
+ $this->saveConfiguration($type, $method_weights);
+ }
+ }
+ else {
+ // The language type is locked. Locked language types with default
+ // settings are always considered non-configurable. In turn if default
+ // settings are missing, the language type is always considered
+ // configurable.
+
+ // If the language type is locked we can just store its default language
+ // negotiation settings if it has some, since it is not configurable.
+ if ($has_default_settings) {
+ $method_weights = [];
+ // Default settings are in $info['fixed'].
+
+ foreach ($info['fixed'] as $weight => $method_id) {
+ if (isset($method_definitions[$method_id])) {
+ $method_weights[$method_id] = $weight;
+ }
+ }
+ $this->saveConfiguration($type, $method_weights);
+ }
+ else {
+ // It was missing default settings, so force it to be configurable.
+ $configurable = TRUE;
+ }
+ }
+
+ // Accumulate information for each language type so it can be saved later.
+ $language_types[$type] = $configurable;
+ }
+
+ // Store the language type configuration.
+ $config = [
+ 'configurable' => array_keys(array_filter($language_types)),
+ 'all' => array_keys($language_types),
+ ];
+ $this->languageManager->saveLanguageTypesConfiguration($config);
+ }
+
+}