Are there anyone that plan to work on a D8 version of this module?

LanguageManager::getFallbackCandidates

There is now support for string fallback in core:
#2122175: String translation does not honor language fallback

So we should be able to choose for each language if we want fallback for strings only, or also content/entity fallback.

Maybe we could also make a hook to make it possible for more fine-adjusting custom fallback logics, I can imagine cases where we want some specific entities/content-types to always fallback to default language, or always fallback to default language for registered users, or make it possible to create country dependent fallback languages (like we have now in D7), but move that part out of the module. So a hook for custom solutions would be great.

I also think this issue can be worth to check out:
#2019055: Switch from field-level language fallback to entity-level language fallback

Hope more people will come with their thought on a D8 release, especially the maintainers.

Comments

matsbla’s picture

@Jelle_S, attiks:
Are there any plans? Do you need help?

Please give your feedback

attiks’s picture

For the moment we don't have the time (or client / funding) to do it, if you want to give it a go, feel free and we will review the patches.

I think entity fallback is the most sane approach

gábor hojtsy’s picture

Status: Active » Needs review

Here, I ported the module, you just need to replace "sublanguage" with "language_fallback". Needed it for a session I am doing *tomorrow*:

sublanguage.info.yml:

name: Sublanguage
type: module
description: 'Allows users to configure inheritance between languages.'
package: Multilingual
version: VERSION
core: 8.x

sublanguage.module


/**
 * @file
 * Add sublanguage handling functionality to Drupal.
 */

use Drupal\Core\Form\FormStateInterface;
use Drupal\language\ConfigurableLanguageInterface;
use Drupal\language\Entity\ConfigurableLanguage;

/**
 * Implements hook_language_fallback_candidates_OPERATION_alter()
 */
function sublanguage_language_fallback_candidates_locale_lookup_alter(array &$candidates, array $context) {
  $attempted_langcode = $context['langcode'];
  $candidates = array();

  /** @var Drupal\language\Entity\ConfigurableLanguage $language */
  $language = ConfigurableLanguage::load($attempted_langcode);
  do {
    $fallback_langcode = $language->getThirdpartySetting('sublanguage', 'fallback_langcode', '');
    // Include this candidate if there was a fallback language and it was not
    // the same as the original langcode (which LocaleLookup already tried) and
    // if it is not already in the list. Avoid endless loops and fruitless work.
    if (!empty($fallback_langcode) && $attempted_langcode != $fallback_langcode && !in_array($fallback_langcode, $candidates)) {
      $candidates[] = $fallback_langcode;
      $language = ConfigurableLanguage::load($fallback_langcode);
    }
    else {
      $language = NULL;
    }
  } while (!empty($language));
}

/**
 * Implements hook_form_FORM_ID_alter()
 */
function sublanguage_form_language_admin_edit_form_alter(&$form, FormStateInterface $form_state) {
  /** @var Drupal\language\Entity\ConfigurableLanguage $this_language */
  $this_language = $form_state->getFormObject()->getEntity();

  $languages = Drupal::languageManager()->getLanguages();
  $options = array();
  foreach($languages as $language) {
    // Only include this language if its not itself.
    if ($language->getId() != $this_language->getId()) {
      $options[$language->getId()] = $language->getName();
    }
  }
  $form['sublanguage_fallback_langcode'] = array(
    '#type' => 'select',
    '#title' => t('Interface translation fallback language'),
    '#description' => t('When an interface translation is not available for text, this fallback language is used. If that is not available either, the fallback continues onward.'),
    '#options' => $options,
    '#default_value' => $this_language->getThirdPartySetting('sublanguage', 'fallback_langcode', ''),
    // Allow to not fall back on any other language.
    '#empty_option' => t('-None-'),
  );
  $form['#entity_builders'][] = 'sublanguage_form_language_admin_edit_form_builder';
}

/**
 * Entity builder for the language form sublanguage options.
 *
 * @see sublanguage_form_language_admin_edit_form_alter()
 */
function sublanguage_form_language_admin_edit_form_builder($entity_type, ConfigurableLanguageInterface $this_language, &$form, FormStateInterface $form_state) {
  $this_language->setThirdPartySetting(
    'sublanguage',
    'fallback_langcode',
    $form_state->getValue('sublanguage_fallback_langcode')
  );
}

config/schema/sublanguage.schema.yml

# Schema for configuration files of the Sublanguage module.

language.entity.*.third_party.sublanguage:
  type: mapping
  label: 'Per language fallback settings'
  mapping:
    fallback_langcode:
      type: string
      label: 'Fallback language code'

Sorry no time for the patch form now.

gábor hojtsy’s picture

StatusFileSize
new2.25 KB
attiks’s picture

@Gábor Hojtsy thanks

  • attiks committed 5860872 on 8.x-1.x authored by Gábor Hojtsy
    Issue #2481685 by Gábor Hojtsy: Port language fallback module to Drupal...
attiks’s picture

Status: Needs review » Fixed

Renamed and committed as 8.x-1.x, dev release will be available in 12 hours.

siliconmind’s picture

Have a look at alternate approach in OOP/Symfony style. It uses custom language_manager service and and implements fallback by overriding ConfigurableLanguageManager::getFallbackCandidates(). The solution uses config files but can be easily be updated to include UI.

I know that those ugly hooks weren't removed from core entirely but I don't see a reason why we should still use them in places where we don't need them.

namespace Drupal\language_fallback;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\language\LanguageServiceProvider;

class LanguageFallbackServiceProvider extends LanguageServiceProvider {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    // All the configuration is done by LanguageServiceProvider
    parent::alter($container);

    // At this point the language_manager definition has been altered
    // by LanguageServiceProvider. We need to modify only one thing...
    $definition = $container->getDefinition('language_manager');

    // We only change the class
    $definition->setClass('Drupal\language_fallback\FallbackLanguageManager');
  }

}
namespace Drupal\language_fallback;

use Drupal\language\ConfigurableLanguageManager;

class FallbackLanguageManager extends ConfigurableLanguageManager {

  /**
   * Cache for fallback candidates.
   *
   * @var array[string]string[]
   */
  private $candidates = array();

  /**
   * {@inheritdoc}
   */
  public function getFallbackCandidates(array $context = array()) {

    if ($this->isMultilingual()) {

      // This is the context that is used for fallback.
      $operation = (isset($context['operation']) ? $context['operation'] : 'default');

      // The language code that we'll use to look for fallback chain.
      $language = $context['langcode'];

      // Read config from yml file if it wasn't loaded yet for requested operation.
      if (!isset($this->candidates[$operation])) {
        // We try not to call \Drupal::config() too often, so we cache the results.
        $config = \Drupal::config('language_fallback.settings');

        // Check if there are fallbacks for requested operation
        $chains = $config->get($operation);

        // If there are no fallback chains and operation wasn't 'default' try loading default operation.
        if (NULL == $chains && $operation != 'default') {
          $chains = $config->get('default');
        }

        // Cache the fallback chains.
        $this->candidates[$operation] = (NULL == $chains ? array() : $chains);
      }

      // Return our configured fallback if available.
      if (isset($this->candidates[$operation][$language])) {
        return $this->candidates[$operation][$language];
      }
    }

    // Return default candidates
    return parent::getFallbackCandidates($context);
  }

}

gábor hojtsy’s picture

@SiliconMind: there is no reason to swap out the whole language manager for this feature, that is why the hook is there. If some other module also wants to swap out the language manager for some other fun thing, then your feature is killed entirely. Using the hooks you can integrate with more modules and your feature is less prone to disappearing entirely.

siliconmind’s picture

StatusFileSize
new3.09 KB

@Gabor I'm aware of that but on the other hand this is actually a core feature and using hooks for it feels just awkward. Especially since we actually run Symfony under the hood. Also hooks hurt performance more than swapping services. Anyway, I'm uploading the whole thing for reference.

gábor hojtsy’s picture

That hook is a core feature as well, that is how my code worked.

jose reyero’s picture

While I agree with @Gábor, I think there's something we are failing to communicate and maybe it should be written somewhere because it may be somehow frustrating for module developers.

IMO there's some missing documentation/policy about "why replacing core services/components is not usually the best practice for contrib modules". Actually I think contrib modules doing that should be getting some special tag as they won't work with other contrib modules doing the same...

@Gábor, do you think this is worth some documentation / policy discussion? Because so far this handbook page seems to be actually encouraging people to replace core services in contrib modules, https://www.drupal.org/node/2026959

gábor hojtsy’s picture

@Jose Reyero: I don't think we need to discuss a "policy" for that, I think its crystal clear. Added this to https://www.drupal.org/node/2026959/revisions/view/7345813/8762001

Note that while swapping services is very easy, you should use caution when making use of this feature. If multiple modules swap the same service, then neither module can predict if their swapped service will win eventually. If a service provides extension capabilities with events, hooks, or by other means, that is definitely more compatible with other extensions.

jose reyero’s picture

@Gábor Hojtsy,
Much better, thanks.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

penyaskito’s picture

Looks like the 8.x-1.x-dev release was never created.

attiks’s picture

Status: Closed (fixed) » Needs work

Strange, on phone right now, will have to check tomorrow

penyaskito’s picture

kalpaitch’s picture

As discussed with @attiks I'm planning to incorporate some of the extra 7.x-2.x functionality. I have a use for it.

attiks’s picture

Status: Needs work » Fixed

Code is cleaned, bate release created

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.