Multilingual content in Drupal 8: a highly evolved permutated API DrupalCamp Vienna 2013 Francesco Placella
Francesco Placella // plach From Venice, Italy Studied at Ca' Foscari University Owner at PSEGNO Drupal since 2006 http://twitter.com/plach
Outline Drupal 7: a bit of history Drupal 8: entity API overview The content translation UI Entity storage and querying The entity translation API What's left?
D7 Field Language API Anatomy of field data $entity->{$field_name}[$langcode][$delta][$column] Field translatability $node->body['en'][0]['value'] $node->body['de'][0]['value'] $node->field_tags[language_none][0]['tid'] Field attach behavior all languages for storage single language for forms and view Good consistency vs bad DX
D7 Entity Language API Incomplete Entity API in core entity_language($langcode) Entity API module $entity_wrapper->language($langcode) $entity->gettranslation($property, $langcode = NULL) Entity Translation module CRUD hooks hook_entity_translation_insert(... ) hook_entity_translation_update(... ) hook_entity_translation_delete(... ) Spread in three places and inconsistent
D7 translation models Two competing approaches to translating content node translation (core) field translation (core API + contrib UI) https://drupal.org/project/entity_translation Why not node translation? only nodes problems with references sharing data across translations
D8 translation model Having two models in core is bad site builders have to choose one developers have to support both everyone has to understand both Increased cognitive / operative / maintenance burden The solution: an unified model every piece of entity data is translatable only one entity for each translation set
D8 Entity API Classed objects encapsulated data interfaces and swappable implementations More (swappable) controllers storage, form, translation, render, access, Entity Field API improved DX! unified API for fields (base / dynamic) field definitions (Typed Data API)
The Content Translation UI Content Content entity Applies to any translatable entity type Translatability has column granularity entity type bundle field column Exploits language-aware entity forms Is enhanced through translation controllers
Entity Storage... Every field needs to be translatable: how? D7 has just a one-off solution Title module D7 fields have native multilingual storage We need multilingual storage for every field The Entity Storage API per-entity storage instead of per-field storage storage-agnostic entities
and Querying No fixed storage layout for fields Data is retrieved by querying + loading Entity Query (EFQ v2) multiple / swappable query back-ends improved condition interface unified interface (similar to DBTNG) AND/OR condition groups support for joins and aggregation Entity caching and lazy-loading
Multilingual entity queries The Entity Query API does not make any assumption on language conditions $result = \Drupal::entityQuery('node') ->condition('promote', 1) ->condition('status', 1) ->execute(); // Nodes with one published/promoted translation $result = \Drupal::entityQuery('node') ->condition('promote', 1) ->condition('status', 1) ->condition('langcode', 'en') ->execute(); // Nodes with one english promoted translation $result = \Drupal::entityQuery('node') ->condition('promote', 1) ->condition('status', 1) ->condition('default_langcode', 1) ->execute(); // Nodes with promoted original values
Core SQL storage Dynamic fields still have per-field tables native multilingual support Base fields get a table layout supporting multilingual base table revision table (base) field data table (base) field revision data table Only the base table is required
SQL storage performance This table layout imposes a performance penalty on monolingual sites on query composite PKs, additional joins on save additional records stored How to mitigate that? automatically generate tables dynamically switch table layout Potential improvement by disabling (unused) revision support
The Entity Translation API Every entity translation is a different translation object (thanks @Crell) $value = $entity->foo->value; $translation = $entity->gettranslation('it'); $it_value = $translation->foo->value; Field language is no longer exposed in the public API Unified and streamlined existing APIs native hooks
Accessing field data We no longer need to worry about field translatability D7 // 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 // Determine the $active_langcode somehow. $translation = $entity->gettranslation($active_langcode); $value = $translation->field_foo->value; The entity system handles it internally
The active language Translation objects are plain entity objects D7 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 } } No need to pass along the active language
The active language (2) D8 $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; $langcode = $entity->language()->id; // do stuff } Just figure out the active language once No need to pass it along Language-agnostic code!
Determining the active language In D7 only features field language fallback function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL, $options = array()) { $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; } field_language() makes sense / can be used only in rendering contexts if a field value is empty another value in a different language is picked
Determining the active language (2) D8 features a reusable entity language negotiation API public function viewentity(entityinterface $entity, $view_mode = 'full', $langcode = NULL) { $langcode = NULL; // Defaults to the current language $translation = $this->entitymanager ->gettranslationfromcontext($entity, $langcode); $build = entity_do_stuff($translation, 'full'); return $build; } negotiation is applied to the whole entity negotiation is alterable by modules empty values will just not be displayed
Determining the active language (3) A context can be provided function node_tokens($type, $tokens, array $data = array(), array $options = array()) { if (!isset($options['langcode'])) { $langcode = Language::LANGCODE_DEFAULT; } // 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 } the default behavior works correctly for rendering and forms
API use cases: shared data Field data is shared among all the translation objects $entity->langcode->value = 'en'; $translation = $entity->gettranslation('it'); $en_value = $entity->field_foo->value; $it_value = $translation->field_foo->value; $entity->field_untranslatable->value = 'foo'; $translation->field_untranslatable->value = 'bar'; $value = $entity->field_untranslatable->value; // $value is 'bar' Updated values are available for all the translation objects
API use cases: instantiating translations A translation object can be instantiated from any entity / translation object $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'
API use cases: translation handling // Acting an all translations. $languages = $entity->gettranslationlanguages(); foreach ($languages as $langcode => $language) { $translation = $entity->gettranslation($langcode); entity_do_stuff($translation); } 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 an exception
API use cases: storage hooks 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()
What is missing? Completing Entity Query conversion https://drupal.org/node/2068325 Dynamic SQL storage multilingual support for all core entity types https://drupal.org/node/1498720 Make base field definitions translatable https://drupal.org/node/2111887 (node) UX improvements
The Migration API Upgrade from D7 migrate node-based translations https://drupal.org/node/1952044 migrate field data https://drupal.org/node/2137917 (contrib) migrate Entity Translation data https://drupal.org/node/2073467 Support dynamic table layout switching when data is available? <g>
More links Entity Translation API https://drupal.org/node/2040323 Entity Storage https://drupal.org/node/1722906 http://bit.ly/1cbftu2 (Prague notes) Content Translation UI http://bit.ly/sjnfzw (celebration video)
Questions?
Thank you!