Commit d988a30f authored by Dries's avatar Dries

Issue #1810370 by plach, kfritsche, berdir, alexpott, xjm, chx: Entity...

Issue #1810370 by plach, kfritsche, berdir, alexpott, xjm, chx: Entity Translation API improvements.
parent 0329b221
......@@ -240,6 +240,35 @@ function hook_entity_update(Drupal\Core\Entity\EntityInterface $entity) {
->execute();
}
/**
* Acts after storing a new entity translation.
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity object of the translation just stored.
*/
function hook_entity_translation_insert(\Drupal\Core\Entity\EntityInterface $translation) {
$variables = array(
'@language' => $translation->language()->name,
'@label' => $translation->getUntranslated()->label(),
);
watchdog('example', 'The @language translation of @label has just been stored.', $variables);
}
/**
* Acts after deleting an entity translation from the storage.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The original entity object.
*/
function hook_entity_translation_delete(\Drupal\Core\Entity\EntityInterface $translation) {
$languages = language_list();
$variables = array(
'@language' => $languages[$langcode]->name,
'@label' => $entity->label(),
);
watchdog('example', 'The @language translation of @label has just been deleted.', $variables);
}
/**
* Act before entity deletion.
*
......
......@@ -14,7 +14,6 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Database\Connection;
......@@ -289,6 +288,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
$data = $query->execute();
$field_definition = \Drupal::entityManager()->getFieldDefinitions($this->entityType);
$translations = array();
if ($this->revisionTable) {
$data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_table']), drupal_schema_fields_sql($this->entityInfo['base_table'])));
}
......@@ -302,6 +302,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
// Field values in default language are stored with
// Language::LANGCODE_DEFAULT as key.
$langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT;
$translations[$id][$langcode] = TRUE;
foreach ($field_definition as $name => $definition) {
// Set only translatable properties, unless we are dealing with a
......@@ -317,7 +318,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
foreach ($entities as $id => $values) {
$bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE;
// Turn the record into an entity class.
$entities[$id] = new $this->entityClass($values, $this->entityType, $bundle);
$entities[$id] = new $this->entityClass($values, $this->entityType, $bundle, array_keys($translations[$id]));
}
}
}
......@@ -367,6 +368,9 @@ public function save(EntityInterface $entity) {
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
$this->invokeHook('update', $entity);
if ($this->dataTable) {
$this->invokeTranslationHooks($entity);
}
}
else {
$return = drupal_write_record($this->entityInfo['base_table'], $record);
......@@ -412,7 +416,7 @@ public function save(EntityInterface $entity) {
*/
protected function saveRevision(EntityInterface $entity) {
$return = $entity->id();
$default_langcode = $entity->language()->id;
$default_langcode = $entity->getUntranslated()->language()->id;
if (!$entity->isNewRevision()) {
// Delete to handle removed values.
......@@ -422,9 +426,9 @@ protected function saveRevision(EntityInterface $entity) {
->execute();
}
$languages = $this->dataTable ? $entity->getTranslationLanguages(TRUE) : array($default_langcode => $entity->language());
$languages = $this->dataTable ? $entity->getTranslationLanguages() : array($default_langcode => $entity->language());
foreach ($languages as $langcode => $language) {
$translation = $entity->getTranslation($langcode, FALSE);
$translation = $entity->getTranslation($langcode);
$record = $this->mapToRevisionStorageRecord($translation);
$record->langcode = $langcode;
$record->default_langcode = $langcode == $default_langcode;
......@@ -511,7 +515,7 @@ protected function mapToStorageRecord(EntityInterface $entity) {
* @return \stdClass
* The record to store.
*/
protected function mapToRevisionStorageRecord(ComplexDataInterface $entity) {
protected function mapToRevisionStorageRecord(EntityInterface $entity) {
$record = new \stdClass();
$definitions = $entity->getPropertyDefinitions();
foreach (drupal_schema_fields_sql($this->entityInfo['revision_table']) as $name) {
......@@ -534,10 +538,10 @@ protected function mapToRevisionStorageRecord(ComplexDataInterface $entity) {
* The record to store.
*/
protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
$default_langcode = $entity->language()->id;
$default_langcode = $entity->getUntranslated()->language()->id;
// Don't use strict mode, this way there's no need to do checks here, as
// non-translatable properties are replicated for each language.
$translation = $entity->getTranslation($langcode, FALSE);
$translation = $entity->getTranslation($langcode);
$definitions = $translation->getPropertyDefinitions();
$schema = drupal_get_schema($this->entityInfo['data_table']);
......
......@@ -9,6 +9,7 @@
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\user\UserInterface;
use IteratorAggregate;
......@@ -294,10 +295,13 @@ public function language() {
/**
* Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
*
* @return \Drupal\Core\Entity\EntityInterface
*/
public function getTranslation($langcode, $strict = TRUE) {
public function getTranslation($langcode) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
return $this;
}
/**
......@@ -588,4 +592,46 @@ public static function postLoad(EntityStorageControllerInterface $storage_contro
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
}
/**
* {@inheritdoc}
*/
public function getUntranslated() {
return $this->getTranslation(Language::LANGCODE_DEFAULT);
}
/**
* {@inheritdoc}
*/
public function hasTranslation($langcode) {
$translations = $this->getTranslationLanguages();
return isset($translations[$langcode]);
}
/**
* {@inheritdoc}
*/
public function addTranslation($langcode, array $values = array()) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
/**
* {@inheritdoc}
*/
public function removeTranslation($langcode) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
/**
* {@inheritdoc}
*/
public function initTranslation($langcode) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
}
......@@ -138,7 +138,7 @@ public function &__get($name) {
// Language::LANGCODE_DEFAULT. This is necessary as EntityNG always keys
// default language values with Language::LANGCODE_DEFAULT while field API
// expects them to be keyed by langcode.
$langcode = $this->decorated->language()->id;
$langcode = $this->decorated->getUntranslated()->language()->id;
if ($langcode != Language::LANGCODE_DEFAULT && isset($this->decorated->values[$name]) && is_array($this->decorated->values[$name])) {
if (isset($this->decorated->values[$name][Language::LANGCODE_DEFAULT]) && !isset($this->decorated->values[$name][$langcode])) {
$this->decorated->values[$name][$langcode] = &$this->decorated->values[$name][Language::LANGCODE_DEFAULT];
......@@ -424,8 +424,8 @@ public function getTranslationLanguages($include_default = TRUE) {
/**
* Forwards the call to the decorated entity.
*/
public function getTranslation($langcode, $strict = TRUE) {
return $this->decorated->getTranslation($langcode, $strict);
public function getTranslation($langcode) {
return $this->decorated->getTranslation($langcode);
}
/**
......@@ -526,12 +526,6 @@ public function onChange($property_name) {
$this->decorated->onChange($property_name);
}
/**
* Forwards the call to the decorated entity.
*/
public function isTranslatable() {
return $this->decorated->isTranslatable();
}
/**
* Forwards the call to the decorated entity.
......@@ -588,4 +582,47 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
}
/**
* Forwards the call to the decorated entity.
*/
public function isTranslatable() {
return $this->decorated->isTranslatable();
}
/**
* Forwards the call to the decorated entity.
*/
public function getUntranslated() {
return $this->decorated->getUntranslated();
}
/**
* Forwards the call to the decorated entity.
*/
public function hasTranslation($langcode) {
return $this->decorated->hasTranslation($langcode);
}
/**
* Forwards the call to the decorated entity.
*/
public function addTranslation($langcode, array $values = array()) {
return $this->decorated->addTranslation($langcode, $values);
}
/**
* Forwards the call to the decorated entity.
*/
public function removeTranslation($langcode) {
$this->decorated->removeTranslation($langcode);
}
/**
* Forwards the call to the decorated entity.
*/
public function initTranslation($langcode) {
$this->decorated->initTranslation($langcode);
}
}
......@@ -146,6 +146,11 @@ protected function init(array &$form_state) {
// module-provided form handlers there.
$form_state['controller'] = $this;
// Ensure we act on the translation object corresponding to the current form
// language.
$this->entity = $this->getTranslatedEntity($form_state);
// Prepare the entity to be presented in the entity form.
$this->prepareEntity();
$form_display = entity_get_render_form_display($this->entity, $this->getOperation());
......@@ -188,7 +193,7 @@ public function form(array $form, array &$form_state) {
// new entities.
$form['langcode'] = array(
'#type' => 'value',
'#value' => !$entity->isNew() ? $entity->langcode : language_default()->id,
'#value' => !$entity->isNew() ? $entity->getUntranslated()->language()->id : language_default()->id,
);
}
return $form;
......@@ -393,7 +398,6 @@ public function delete(array $form, array &$form_state) {
*/
public function getFormLangcode(array $form_state) {
$entity = $this->entity;
$translations = $entity->getTranslationLanguages();
if (!empty($form_state['langcode'])) {
$langcode = $form_state['langcode'];
......@@ -402,6 +406,7 @@ public function getFormLangcode(array $form_state) {
// If no form langcode was provided we default to the current content
// language and inspect existing translations to find a valid fallback,
// if any.
$translations = $entity->getTranslationLanguages();
$langcode = language(Language::TYPE_CONTENT)->id;
$fallback = language_multilingual() ? language_fallback_get_candidates() : array();
while (!empty($langcode) && !isset($translations[$langcode])) {
......@@ -411,14 +416,14 @@ public function getFormLangcode(array $form_state) {
// If the site is not multilingual or no translation for the given form
// language is available, fall back to the entity language.
return !empty($langcode) ? $langcode : $entity->language()->id;
return !empty($langcode) ? $langcode : $entity->getUntranslated()->language()->id;
}
/**
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::isDefaultFormLangcode().
*/
public function isDefaultFormLangcode(array $form_state) {
return $this->getFormLangcode($form_state) == $this->entity->language()->id;
return $this->getFormLangcode($form_state) == $this->entity->getUntranslated()->language()->id;
}
/**
......@@ -447,13 +452,11 @@ protected function submitEntityLanguage(array $form, array &$form_state) {
$entity_type = $entity->entityType();
if (field_has_translation_handler($entity_type)) {
$form_langcode = $this->getFormLangcode($form_state);
// If we are editing the default language values, we use the submitted
// entity language as the new language for fields to handle any language
// change. Otherwise the current form language is the proper value, since
// in this case it is not supposed to change.
$current_langcode = $entity->language()->id == $form_langcode ? $form_state['values']['langcode'] : $form_langcode;
$current_langcode = $this->isDefaultFormLangcode($form_state) ? $form_state['values']['langcode'] : $this->getFormLangcode($form_state);
foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) {
$field_name = $instance['field_name'];
......@@ -489,10 +492,22 @@ public function buildEntity(array $form, array &$form_state) {
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::getEntity().
*/
public function getEntity() {
// @todo Pick the proper translation object based on the form language here.
return $this->entity;
}
/**
* Returns the translation object corresponding to the form language.
*
* @param array $form_state
* A keyed array containing the current state of the form.
*/
protected function getTranslatedEntity(array $form_state) {
$langcode = $this->getFormLangcode($form_state);
$translation = $this->entity->getTranslation($langcode);
// Ensure that the entity object is a BC entity if the original one is.
return $this->entity instanceof EntityBCDecorator ? $translation->getBCEntity() : $translation;
}
/**
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::setEntity().
*/
......@@ -521,7 +536,7 @@ protected function prepareInvokeAll($hook, array &$form_state) {
if (function_exists($function)) {
// Ensure we pass an updated translation object and form display at
// each invocation, since they depend on form state which is alterable.
$args = array($this->getEntity(), $this->getFormDisplay($form_state), $this->operation, &$form_state);
$args = array($this->getTranslatedEntity($form_state), $this->getFormDisplay($form_state), $this->operation, &$form_state);
call_user_func_array($function, $args);
}
}
......
......@@ -61,11 +61,10 @@ public function buildEntity(array $form, array &$form_state) {
// edited by this form. Values of fields handled by field API are copied
// by field_attach_extract_form_values() below.
$values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values'];
$translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE);
$definitions = $translation->getPropertyDefinitions();
$definitions = $entity->getPropertyDefinitions();
foreach ($values_excluding_fields as $key => $value) {
if (isset($definitions[$key])) {
$translation->$key = $value;
$entity->$key = $value;
}
}
......
......@@ -323,4 +323,15 @@ public function getNGEntity();
*/
public function isTranslatable();
/**
* Marks the translation identified by the given language code as existing.
*
* @todo Remove this as soon as translation metadata have been converted to
* regular fields.
*
* @param string $langcode
* The language code identifying the translation to be initialized.
*/
public function initTranslation($langcode);
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use InvalidArgumentException;
......@@ -25,6 +26,21 @@
*/
class EntityNG extends Entity {
/**
* Status code indentifying a removed translation.
*/
const TRANSLATION_REMOVED = 0;
/**
* Status code indentifying an existing translation.
*/
const TRANSLATION_EXISTING = 1;
/**
* Status code indentifying a newly created translation.
*/
const TRANSLATION_CREATED = 2;
/**
* Local cache holding the value of the bundle field.
*
......@@ -67,6 +83,13 @@ class EntityNG extends Entity {
*/
protected $language;
/**
* Local cache for the available language objects.
*
* @var array
*/
protected $languages;
/**
* Local cache for field definitions.
*
......@@ -83,12 +106,42 @@ class EntityNG extends Entity {
*/
protected $uriPlaceholderReplacements;
/**
* Language code identifying the entity active language.
*
* This is the language field accessors will use to determine which field
* values manipulate.
*
* @var string
*/
protected $activeLangcode = Language::LANGCODE_DEFAULT;
/**
* An array of entity translation metadata.
*
* An associative array keyed by translation language code. Every value is an
* array containg the translation status and the translation object, if it has
* already been instantiated.
*
* @var array
*/
protected $translations = array();
/**
* A flag indicating whether a translation object is being initialized.
*
* @var bool
*/
protected $translationInitialize = FALSE;
/**
* Overrides Entity::__construct().
*/
public function __construct(array $values, $entity_type, $bundle = FALSE) {
public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) {
$this->entityType = $entity_type;
$this->bundle = $bundle ? $bundle : $this->entityType;
$this->languages = language_list(Language::STATE_ALL);
foreach ($values as $key => $value) {
// If the key matches an existing property set the value to the property
// to ensure non converted properties have the correct value.
......@@ -97,6 +150,20 @@ public function __construct(array $values, $entity_type, $bundle = FALSE) {
}
$this->values[$key] = $value;
}
// Initialize translations. Ensure we have at least an entry for the entity
// original language.
$data = array('status' => static::TRANSLATION_EXISTING);
$this->translations[Language::LANGCODE_DEFAULT] = $data;
if ($translations) {
$default_langcode = $this->language()->id;
foreach ($translations as $langcode) {
if ($langcode != $default_langcode && $langcode != Language::LANGCODE_DEFAULT) {
$this->translations[$langcode] = $data;
}
}
}
$this->init();
}
......@@ -117,11 +184,24 @@ protected function init() {
unset($this->langcode);
}
/**
* Clear entity translation object cache to remove stale references.
*/
protected function clearTranslationCache() {
foreach ($this->translations as &$translation) {
unset($translation['entity']);
}
}
/**
* Magic __wakeup() implementation.
*/
public function __wakeup() {
$this->init();
// @todo This should be done before serializing the entity, but we would
// need to provide the full list of data to be serialized. See the
// dedicated issue at https://drupal.org/node/2027795.
$this->clearTranslationCache();
}
/**
......@@ -204,12 +284,10 @@ protected function uriPlaceholderReplacements() {
* Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
*/
public function get($property_name) {
// Values in default language are always stored using the
// Language::LANGCODE_DEFAULT constant.
if (!isset($this->fields[$property_name][Language::LANGCODE_DEFAULT])) {
return $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT);
if (!isset($this->fields[$property_name][$this->activeLangcode])) {
return $this->getTranslatedField($property_name, $this->activeLangcode);
}
return $this->fields[$property_name][Language::LANGCODE_DEFAULT];
return $this->fields[$property_name][$this->activeLangcode];
}
/**
......@@ -218,6 +296,10 @@ public function get($property_name) {
* @return \Drupal\Core\Entity\Field\FieldInterface
*/
protected function getTranslatedField($property_name, $langcode) {
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
$message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
throw new \InvalidArgumentException(format_string($message, array('@langcode' => $this->activeLangcode)));
}
// Populate $this->fields to speed-up further look-ups and to keep track of
// fields objects, possibly holding changes to field values.
if (!isset($this->fields[$property_name][$langcode])) {
......@@ -236,9 +318,9 @@ protected function getTranslatedField($property_name, $langcode) {
$value = $this->values[$property_name][$langcode];
}
// @todo Remove this once the BC decorator is gone.
elseif ($property_name != 'langcode') {
elseif ($property_name != 'langcode' && $langcode == Language::LANGCODE_DEFAULT) {
$default_langcode = $this->language()->id;
if ($langcode == Language::LANGCODE_DEFAULT && isset($this->values[$property_name][$default_langcode])) {
if (isset($this->values[$property_name][$default_langcode])) {
$value = $this->values[$property_name][$default_langcode];
}
}
......@@ -337,15 +419,42 @@ public function isEmpty() {
}
/**
* Implements \Drupal\Core\TypedData\TranslatableInterface::language().
* {@inheritdoc}
*/
public function access($operation = 'view', AccountInterface $account = NULL) {
return \Drupal::entityManager()
->getAccessController($this->entityType)
->access($this, $operation, $this->activeLangcode, $account);
}
/**
* {@inheritdoc}
*/
public function language() {
if ($this->activeLangcode != Language::LANGCODE_DEFAULT) {
if (!isset($this->languages[$this->activeLangcode])) {
$this->languages += language_list(Language::STATE_ALL);
}
return $this->languages[$this->activeLangcode];
}
else {
return $this->language ?: $this->getDefaultLanguage();
}
}
/**
* Returns the entity original language.
*
* @return \Drupal\Core\Language\Language
* A language object.
*/
protected function getDefaultLanguage() {
// Keep a local cache of the language object and clear it if the langcode
// gets changed, see EntityNG::onChange().
if (!isset($this->language)) {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode')) {
$this->language = $this->get('langcode')->language;
if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) {
$this->language = $item->language;
}
if (empty($this->language)) {
// Make sure we return a proper language object.
......@@ -369,77 +478,182 @@ public function onChange($property_name) {
/**
* Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
*
* @return \Drupal\Core\Entity\Plugin\DataType\EntityTranslation
*/
public function getTranslation($langcode, $strict = TRUE) {
// If the default language is Language::LANGCODE_NOT_SPECIFIED, the entity is not
// translatable, so we use Language::LANGCODE_DEFAULT.
if ($langcode == Language::LANGCODE_DEFAULT || in_array($this->language()->id, array(Language::LANGCODE_NOT_SPECIFIED, $langcode))) {
// No translation needed, return the entity.
return $this;
* @return \Drupal\Core\Entity\EntityInterface
*/
public function getTranslation($langcode) {
// Ensure we always use the default language code when dealing with the
// original entity language.
if ($langcode != Language::LANGCODE_DEFAULT) {
$default_language = $this->language ?: $this->getDefaultLanguage();
if ($langcode == $default_language->id) {
$langcode = Language::LANGCODE_DEFAULT;
}
}
// Check whether the language code is valid, thus is of an available
// language.
$languages = language_list(Language::STATE_ALL);
if (!isset($languages[$langcode])) {
throw new InvalidArgumentException("Unable to get translation for the invalid language '$langcode'.");
// Populate entity translation object cache so it will be available for all
// translation objects.
if ($langcode == $this->activeLangcode) {
$this->translations[$langcode]['entity'] = $this;
}
$fields = array();
foreach ($this->getPropertyDefinitions() as $name => $definition) {
// Load only translatable properties in strict mode.
if (!empty($definition['translatable']) || !$strict) {
$fields[$name] = $this->getTranslatedField($name, $langcode);
// If we already have a translation object for the specified language we can
// just return it.
if (isset($this->translations[$langcode]['entity'])) {
$translation = $this->translations[$langcode]['entity'];
}
else {
if (isset($this->translations[$langcode])) {
$translation = $this->initializeTranslation($langcode);
$this->translations[$langcode]['entity'] = $translation;
}
else {
// If we were given a valid language and there is no translation for it,
// we return a new one.
$languages = language_list(Language::STATE_ALL);
if (isset($languages[$langcode])) {
// If the entity or the requested language is not a configured
// language, we fall back to the entity itself, since in this case it
// cannot have translations.
$translation = empty($this->getDefaultLanguage()->locked) && empty($languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this;
}
}
}
if (empty($translation)) {
$message = 'Invalid translation language (@langcode) specified.';
throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode)));
}
// @todo: Add a way to get the definition of a translation to the
// TranslatableInterface and leverage TypeDataManager::getPropertyInstance
// also.
$translation_definition = array(
'type' => 'entity_translation',
'constraints' => array(
'entity type' => $this->entityType(),
'bundle' => $this->bundle(),
),
);
$translation = \Drupal::typedData()->create($translation_definition, $fields);
$translation->setStrictMode($strict);
$translation->setContext('@' . $langcode, $this);
return $translation;
}
/**
* Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
* {@inheritdoc}
*/
public function getTranslationLanguages($include_default = TRUE) {