Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

In Drupal 7 the Field Language API provides native multilingual support for the Field API. This means that any field attached to an entity can theoretically hold a value for every installed language. This is great to provide a generic entity translation feature but has the big downside of making the DX of accessing field values awful, in fact this is how a multilingual field data structure may appear:

<?php
  $entity
->field_foo = array(
   
'en' => array(
     
0 => array(
       
'value' => 'bar',
      ),
     
1 => array(
       
'value' => 'baz',
      ),
    ),
   
'it' => array(
     
0 => array(
       
'value' => 'bella',
      ),
     
1 => array(
       
'value' => 'zio',
      ),
    ),
  );
?>

and this is the way to access a field item value:

<?php
  $value
= $entity->field_foo[$langcode][$delta][$column];
?>

While $delta and $column are usually known, $langcode is way trickier to determine. As a matter of fact a field can be untranslatable, in which case the value provided by the LANGUAGE_NONE constant can be used as the field language, or translatable, meaning it can hold different values per language. In the latter case determining which language we should act on is not always straightforward and usually depends on the implemented logic. The core Field Language API acts on every available field translation when dealing with storage operations, that is loading and saving a field implies retrieving and storing all translations. Instead when performing other tasks, such as rendering a field or displaying a form field widget, a single field translation is picked. The language of field translations is usually derived from the active language, which may be the current content language, if displaying an entity, or the language of the entity translation being edited when displaying an entity form.

Having to take into account all of this just to access a field value is really bad DX. For this reason the approach has radically changed in Drupal 8: field language is no longer exposed in the public API, instead fields are attached to language-aware entity objects from which they "inherit" their language.

The major advantages here are:

  • We do not need to worry about field translatability, as this is taken care of by the entity object internally.

    D7:

        <?php
         
    // Determine the $active_langcode somehow.
         
    $field = field_info_field('field_foo');
         
    $langcode = field_is_translatable($entity_type, $field) ? $active_langcode : LANGUAGE_NONE;
         
    $value = $entity->field_foo[$langcode][0]['value'];
       
    ?>

       

    D8:

        <?php
         
    // Determine the $active_langcode somehow.
         
    $translation = $entity->getTranslation($active_langcode);
         
    $value = $translation->field_foo->value;
       
    ?>

       
  • We no longer need to pass around the active language, in fact we can just pass around the translation object, which implements EntityInterface and is actually a clone of the original object, just with a different internal language. This means in many cases the resulting code can be language-unaware (of course if it is not explictly dealing with language).

    D7:

        <?php
         
    function entity_do_stuff($entity, $langcode = NULL) {
            if (!isset(
    $langcode)) {
             
    $langcode = $GLOBALS['language_content']->language;
            }
           
    $field = field_info_field('field_foo');
           
    $langcode = field_is_translatable($entity_type, $field) ? $langcode : LANGUAGE_NONE;
            if (!empty(
    $entity->field_foo[$langcode])) {
             
    $value = $entity->field_foo[$langcode][0]['value'];
             
    // do stuff
           
    }
          }
       
    ?>

       

    D8:

        <?php
         
    // Instantiate the proper translation object just once and pass it around
          // wherever it is needed. This is typically taken care of by core
          // subsystems and in many common cases an explicit retrieval of the
          // translation object is not needed.
         
    $langcode = Drupal::languageManager()->getLanguage(Language::TYPE_CONTENT);
         
    $translation = $entity->getTranslation($langcode);
         
    entity_do_stuff($translation);

          function

    entity_do_stuff(EntityInterface $entity) {
           
    $value = $entity->field_foo->value;
           
    // do stuff
         
    }
       
    ?>

       
  • While in D7 we had only the concept of field language fallback, which was closely tied to the rendering phase, we have now a reusable entity language negotiation API, that can be used to determine the entity translation that is most appropriate for a certain context. This allowed us to remove the confusing field-level language fallback, which caused empty field values not to behave as desired in most use cases.

    D7:

        <?php
         
    // Simplified code to generate a renderable array for field values.
         
    function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL, $options = array()) {
           
    // field_language() applies field fallback logic, which makes sense and
            // can be used only in a rendering context. If a field value is empty
            // another non-empty value in a different language will be picked.
           
    $display_language = field_language($entity_type, $entity, NULL, $langcode);
           
    $options['language'] = $display_language;
           
    $null = NULL;
           
    $output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
            return
    $output;
          }
       
    ?>

     

    D8:

        <?php
         
    // Simplified code to generate a renderable array for an entity.
         
    function viewEntity(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
           
    // The EntityManagerInterface::getTranslationFromContext() method will
            // apply entity language negotiation logic to the whole entity object
            // and will return the proper translation object for the given context.
            // The $langcode parameter is optional and indicates the language of the
            // current context. If it is not specified the current content language
            // is used, which is the desired behavior during the rendering phase.
            // Note that field values are left alone in the process, so empty values
            // will just not be displayed.
           
    $langcode = NULL;
           
    $translation = $this->entityManager->getTranslationFromContext($entity, $langcode);
           
    $build = entity_do_stuff($translation, 'full');
            return
    $build;
          }
       
    ?>

     

    We can also specify an optional $context parameter, which can be used to describe the context where the translation object will be used:

        <?php
         
    // Simplified token relpacements generation code.
         
    function node_tokens($type, $tokens, array $data = array(), array $options = array()) {
           
    $replacements = array();

           

    // If no language is specified for this context we just default to the
            // default entity language.
           
    if (!isset($options['langcode'])) {
             
    $langcode = Language::LANGCODE_DEFAULT;
            }

           

    // We pass a $context parameter describing the operation being performed.
            // The default operation is 'entity_view'.
           
    $context = array('operation' => 'node_tokens');
           
    $translation = \Drupal::entityManager()->getTranslationFromContext($data['node'], $langcode, $context);
           
    $items = $translation->get('body');

           

    // do stuff

           

    return $replacements;
          }
       
    ?>

     

    The logic used to determine the translation object to be returned is alterable by modules. See the related change notice for more details.

The actual field data is shared among all the translation objects and changing the value of an untranslatable field automatically changes it for all the translation objects.

<?php
  $entity
->langcode->value = 'en';
 
$translation = $entity->getTranslation('it');

 

$en_value = $entity->field_foo->value; // $en_value is 'bar'
 
$it_value = $translation->field_foo->value; // $it_value is 'bella'

 

$entity->field_untranslatable->value = 'baz';
 
$translation->field_untranslatable->value = 'zio';
 
$value = $entity->field_untranslatable->value; // $value is 'zio'
?>

In any moment a translation object can be instantiated from the original object or another translation object through the EntityInterface::getTranslation() method. If the active language is explicitly needed, it can be retrieved through EntityInterface::language(). The original entity can be retrieved through EntityInterface::getUntranslated().

<?php
  $entity
->langcode->value = 'en';

 

$translation = $entity->getTranslation('it');
 
$langcode = $translation->language()->id; // $langcode is 'it';

 

$untranslated_entity = $translation->getUntranslated();
 
$langcode = $untranslated_entity->language()->id; // $langcode is 'en';

 

$identical = $entity === $untranslated_entity; // $identical is TRUE

 

$entity_langcode = $translation->getUntranslated()->language()->id; // $entity_langcode is 'en'
?>

EntityInterface has now several methods that make easier to deal with entity translations. If a piece of code needs to act on every available translation, it can exploit EntityInterface::getTranslationLanguages():

<?php
 
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
   
$translation = $entity->getTranslation($langcode);
   
entity_do_stuff($translation);
  }
?>

There are also methods to add a translation, remove it or check for its existence:

<?php
 
if (!$entity->hasTranslation('fr')) {
   
$translation = $entity->addTranslation('fr', array('field_foo' => 'bag'));
  }

 

// Which is equivalent to the following code, although if an invalid language
  // code is specified an exception is thrown.
 
$translation = $entity->getTranslation('fr');
 
$translation->field_foo->value = 'bag';

 

// Accessing a field on a removed translation object causes an exception to
  // be thrown.
 
$translation = $entity->getTranslation('it');
 
$entity->removeTranslation('it');
 
$value = $translation->field_foo->value; // throws InvalidArgumentException
?>

When entity translations are added to or removed from the storage the following hooks are fired respectively:

  • hook_entity_translation_insert()
  • hook_entity_translation_delete()

Replacing the D7 Field Language API

  • field_valid_language(): no longer needed as all checks on field language are performed internally by the Entity class. If you need to perform entity language negotiation and retrieve a valid entity translation just use the entity manager:
    <?php
      $translation
    = \Drupal::entityManager()->getTranslationFromContext($entity, $langcode);
    ?>
  • field_language(): this is no longer needed to access field values. If you need to perform entity language negotiation and retrieve a valid entity translation just use the entity manager (see above). If you need to know the language of a field item you can just retrieve it:
    <?php
      $langcode
    = $translation->field_foo->getLangcode();
    ?>
  • field_content_languages(): in D8 content can be assigned any language available on the site, here is how you can retrieve this list:
    <?php
      $languages
    = \Drupal::languageManager()->getLanguageList(Language::STATE_ALL);
    ?>
  • field_available_languages(): as field language is now mostly handled internally you should no longer need this. If a field is translatable it may be assigned any installed language (see above). The language list is no longer alterable (hook_field_available_languages_alter() was removed), since no valid use case for this was left in D8. In fact the main use case for this was implementing a UI to deal with language of parts, but in D8 a composite field holding the value and its language is the best way to achieve that. If a field is untranslatable, it will just have the entity default language. See the related change notice for details.
  • field_has_translation_handler(): the concept of translation handler no longer exists in D8. A field is translatable if it is defined as such, it will be possible to translate it only if the bundle it is attached to is actually translatable:
    <?php
     
    if ($entity->isTranslatable()) {
       
    // do stuff
     
    }
    ?>
  • field_is_translatable(): just use the field definition method:
    <?php
     
    if ($field_definition->isTranslatable()) {
       
    // do stuff
     
    }
    ?>
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done