all contrib module security updates done

This commit is contained in:
2020-09-24 22:47:32 +02:00
parent 7d87153100
commit 0fbb9c4610
1612 changed files with 116663 additions and 32269 deletions
@@ -1,6 +1,201 @@
Entity Translation 7.x-1.x, xxxx-xx-xx
--------------------------------------
Entity Translation 7.x-1.1, 2019-11-24
--------------------------------------
#2877103 by stefanos.petrakis, Simon Georges, Delphine Lepers, Andras_Szilagyi,
drasgardian, Alexander Fedorenko, plach: Content can not be created in
another language as the one chosen by configuration
#2580231 by danyg, stefanos.petrakis, MariskaTh: Undefined index in
entity_translation.views.inc
#3085046 by stefanos.petrakis: Fix test breakage due to Title module
7.x-1.0-beta1 version
#3010146 by stefanos.petrakis: Expose extended ET-specific configuration for
pathauto
#3050434 by shital.mahajan, jweowu, stefanos.petrakis: Since PHP 7.0, functions
inspecting arguments, like func_get_args(), no longer report the original
value as passed to a parameter
#3025770 by sudheeshps, patelmayank7552, stefanos.petrakis: PHP 7.0
Compatibility
#2536292 by plopesc, stefanos.petrakis: "Warning: array_merge(): Argument #2 is
not an array in entity_translation_menu_alter" When 'page arguments' or
'access arguments' are not defined in the original item
#2921298 by Stevel, stefanos.petrakis: Missing test dependencies
Entity Translation 7.x-1.0, 2018-04-01
--------------------------------------
(no change)
Entity Translation 7.x-1.0-rc1, 2018-03-21
------------------------------------------
#2927815 by Skabbkladden, dawehner: Infinite loop with language neutral alias
and pathauto.
#2890683 by tobiberlin, stefanos.petrakis: entity_translation_field_attach_form()
adding unwanted fields to form.
#2907366 by swati.nuna: Invalid hook hook_translation_info() mentioned in
docblock for entity_translation_edit_form_info().
#1799770 by plach, bforchhammer: Update id and bundle when setting a wrapped
entity.
#2339315 by plach: Added documentation for
hook_entity_translation_source_field_state_alter().
#1506054 by plach, Owen Barton: Taxonomy term reference: language-aware widget
and autocomplete.
#2907275 by jalpesh: CSS class name misspelled in entity_translation_overview().
Entity Translation 7.x-1.0-beta7, 2017-08-21
--------------------------------------------
#2903195 by stefanos.petrakis, BAHbKA, plach: Enabling translation for not
empty taxonomy vocabulary.
#2425825 by jamesrward: Update from before beta4 fails.
#2900094 by joseph.olstad: code sniffer drupal.org recommended code syntax
fixes.
#2883805 by mikran: entity_translation_update_7008 is broken when taxonomy
module is not enabled.
#2877074 by plach, stefanos.petrakis, joseph.olstad: Refactor the
entity_translation_language() callback to make it bundle-specific.
#2743685 by stefanos.petrakis, bwaindwain: Pathauto update for all translations
for a single node.
#2899658 by joseph.olstad, stefanos.petrakis: entity_translation_upgrade_do
needs to automatically create the entity_translation row for the source
language in entity_translation.
#1800158 by meichr: Entity Translation Upgrade, drush extension.
#2885858 by StephaneQ: Can't delete taxonomy term translation since beta6
update.
#2572203 by mattew: Wrong query generated when several Translations
relationships are used.
#2824255 by ccarrascal, plach: Notice: Undefined index for language in
setTranslation().
#2750179 by wadmiraal: Allowed default translation handler to be completely
overridden.
#2307937 by meichr, joseph.olstad: Batch API needs $context['results'] handled
as an array, not a value.
#1175170 by hgoto, Pisco, alberto56: Optionally enable disabled languages for
entity translation.
#2870524 by stefanos.petrakis, joseph.olstad: Field copy fails with content
translation for fields with entity translation enabled.
Entity Translation 7.x-1.0-beta6, 2017-03-03
--------------------------------------------
#2849464 by plach, czigor, stefanos.petrakis: Added an API to set the active
language.
#2438615 by David_Rothstein, matthiasm11: New nodes are always created using
LANGUAGE_NONE (only changed to the correct language during form submission).
#2820454 by Dylan Donkersgoed: PHP notice when attaching single field
with field_attach_form.
#2826297 by Thomas Cys, dwebpoint, stefanos.petrakis: 'clone' is a reserved
keyword introduced in PHP version 5.0 and cannot be invoked as a function.
#2189567 by stefanos.petrakis, jmuzz: Fixed problems enabling and disabling
translation on a field with revisions enabled.
#2798721 by gnucifer: Path scheme not initialized on delete translation
confirmation form.
#2215771 by colan, stefanos.petrakis: Fixed "Undefined index: translate in
EntityTranslationDefaultHandler->entityForm()".
#1829636 by bforchhammer, stefanos.petrakis: Fixed "All languages" suffix not
working nicely with multi-value fields.
#2746045 by fengtan: Converted dsm calls to watchdog calls for path scheme
definition warnings.
#2757601 by ctrnz, aks22: Fixed entity_translation_update_7009() assumes
i18n_taxonomy is enabled.
#1172104 by james.williams, daro.pl: Improved description of the language
fallback option.
#2734373 by plach, consulio: "entity_translation_update_7008" is broken when no
term is translated.
#2734301 by plach, sinasalek: Fixed Numeric value out of range: 1264 Out of
range value for column 'i18n_mode'.
#1661348 by plach, badrange, colan, candelas: Added "I18n Taxonomy" integration.
Entity Translation 7.x-1.0-beta5, 2016-05-08
--------------------------------------------
#929402 by das-peter, bastnic, kristiaanvandeneynde, brockfanning, greenjuls,
make77, steinmb: Added support for migrate module.
#1291388 by Jax, poiu, plach: Added support for multilingual core search.
#2339315 by loopduplicate, gnucifer, plopesc, james.williams: Fixed Source
language prepopulated values should be also included in $form_state['field'].
#2648062 by brunoric, cebasqueira, lucasr, mmchristoph: Fixed Fatal error: Call
to a member function getFormLanguage() on a non-object.
#S156840 by HelenaEksler, plach: Fixed XSS vulnerability in the translation
form page title.
#2444203 by Leksat, badrange, attiks: Show fallback information on the
translation overview.
#2458393 by catch, plach, douggreen: Fixed unindexed query in
EntityTranslationDefaultHandler::loadMultiple().
#2462909 by GeduR: Misspelling translation exists help in views handler
definition.
#2166157 by rsmylski, ciss: entity translation handler id for new entities not
unique enough - can cause infinite/circular child reference.
#2452279 by GeduR: Duplicated variable $language in
entity_translation_edit_access().
#2423661 by marcoscano: Module api documentation has wrong example ("path sets"
instead of "path schemes").
#2305547 by das-peter: Fixed undefined index name.
#1989084 by jsacksick, candelas: Fixed <nolink> and validation.
#2203801 by idflood: Impossible to update taxonomy term after enabling
i18n_taxonomy.
#2415189 by catch: Translation deletion bypasses entity saving.
Entity Translation 7.x-1.0-beta4, 2015-01-23
--------------------------------------------
#2396103 by plach: Fixed [DATA LOSS] Translations deleted.
#2218087 by plach: Fixed PHP Fatal error: Unsupported operand types in
modules/field_ui/field_ui.admin.inc on line 477.
#2396653 by SchnWalter, mongolito404: Fix validation handler for the entity
language widget.
#2395327 by damiankloip: FATAL error when requesting an edit path for a
non-existent entity.
#2342787 by jcisio: Language widget submit handler runs on disabled entity
types.
#2389945 by rafal.enden: Tests don't pass when using different source than
original node language.
#2185523 by leon.nk: Document that entity_translation_enabled_bundle() does not
check whether the entity type is translatable.
#2378195 by Johnny vd Laar: Node edit notice when author of a translation is
deleted.
#2382713 by seanr: entity_translation_update_7006 fails because of SQL error
before variable_set('entity_translation_revision_enabled', FALSE); can run.
#1046282 by plach, good_man, miro_dietiker, Gábor Hojtsy: Make the module work
with revisions.
#2179183 by dkingofpa, Jānis Bebrītis | SocialNicheGuru: Fixed Fatal error:
Unsupported operand types in /entity_translation.module on line 188.
#1605406 by joelrosen, Jose Reyero, paulihuhtiniemi, guillaumev | mindaugasd:
Exposed translated field content to Views so that multiple language
translations can be shown simultaneously.
#2160559 by w3wfr: Warning because of empty value line 675.
#2151503 by SebCorbin: Check for translation access for Add links.
#2130091 by peximo: Media edit dialog integration bug.
#2065221 by catch, plach: Fixed Static cache for handlers should key by $vid.
#1516202 by fabsor, spotzero: Added a CTools access plugin for checking if a
translation is available.
#2027513 by GaëlG, jcisio: Added support for nested values for 'edit form' in
hook_entity_info().
#2073231 by plach: Rename entity_translation_form_language() to
entity_translation_get_existing_language().
#2072865 by douggreen: Fixed Only replace 'delete' button with
'delete translation', don't add one that doesn't exist.
#1865244 by plach | bojanz, agoradesign, ciss, interdruper, joel_osc: Added
support for multiple translation handlers on the same form.
#2055491 by plach: Fixed Synced image fields cannot be emptied.
#1866076 by grndlvl: Added support for non-entity load tokens when declaring
translations.
Entity Translation 7.x-1.0-beta3, 2013-07-23
--------------------------------------------
#2037789 by Jelle_S: Fixed Default to user_access() access callback like drupal
menu does.
#1982140 by bforchhammer | carligraph: Fixed Menu item is deleted when adding a
third translation.
#2000926 by GiorgosK, bforchhammer: Fixed entity_translation_i18n_menu should
check if entity is node before deleting menu links.
#1991452 by twistor: Fixed Strict warnings on testing page.
#1992588 by bforchhammer: Fixed PHP notices on node page (menu-related).
#1979406 by milesw: Field values for node translations not filled correctly when
making a field translatable.
#1969366 by plach | EdNet: Fixed unsupported operand types in
@@ -21,7 +216,7 @@ Entity Translation 7.x-1.x, xxxx-xx-xx
#1947764 by plach: Improve bundle translatability checks.
#1370900 by mojzis, plach | mgladding: Fixed Fatal Error: Cannot access empty
property in translation.handler.inc.
Issue #1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
#1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
content-translation enabled bundles.
#1933742 by das-peter: Fixed Delete translation doesn't flush the entity cache.
#1903024 by fabsor | dimitrileonidas: Fixed SQL error in Views when adding an
@@ -223,7 +418,7 @@ Entity Translation 7.x-1.0-alpha1, 2011-09-07
editing workflow.
#1031370 by joostvdl | rfay: Fixed Translation publishing status is hidden under
collapsed 'publishing options' fieldset (and is hidden by default).
#1003876 by plach, good_man: (follow-up) Fixed original label translation.
#1003876 by plach, good_man: (follow-up) Fixed original label translation.
#1003876 by good_man, plach: Introduced support for translated entity labels.
#936646 by good_man, fietserwin, plach, klonos: Fixed Undefined index access
callback/arguments in entity_translation_menu().
@@ -9,8 +9,9 @@ Project maintainers
The Entity translation maintainers oversee the development of the project as a
whole. The project maintainers for Entity translation are:
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Daniel F. Kudwien 'sun' <http://drupal.org/user/54136>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
- Daniel F. Kudwien 'sun' <https://www.drupal.org/u/sun>
- Stefanos Petrakis 'stefanos.petrakis' <https://www.drupal.org/u/stefanospetrakis>
Component maintainers
@@ -25,14 +26,15 @@ http://drupal.org/node/363367 to find out how to become a component maintainer.
Current component maintainers for Entity translation:
Base system
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
- Benedikt Forchhammer 'bforchhammer' <https://www.drupal.org/u/bforchhammer>
- Stefanos Petrakis 'stefanos.petrakis' <https://www.drupal.org/u/stefanospetrakis>
Menu integration
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
- Benedikt Forchhammer 'bforchhammer' <https://www.drupal.org/u/bforchhammer>
Views integration
- Fabian Sörqvist 'fabsor' <http://drupal.org/user/255704>
- Fabian Sörqvist 'fabsor' <https://www.drupal.org/u/fabsor>
Node translation upgrade
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
@@ -29,9 +29,50 @@ None.
* @todo
-- DEVELOPERS --
--- PHP7 Compatibility detection ---
Use PHP Code Sniffer (via coder) with the PHPCompatibility sniffs installed.
* https://www.drupal.org/project/coder
* https://github.com/PHPCompatibility/PHPCompatibility
Once phpcs is installed, a simple way to add PHPCompatibility is:
* Run 'phpcs -i' to confirm the current recognised coding standards.
Assuming that PHPCompatibility is not already listed...
* Run 'phpcs --config-show' and take note of the "installed_paths" value.
* Run 'composer require phpcompatibility/php-compatibility'
* Establish the absolute /path/to/vendor/phpcompatibility/php-compatibility
which you should now have somewhere.
* Run 'phpcs --config-set installed_paths OLD,NEW' where OLD is the original
value, and NEW is the php-compatibility path.
* Run 'phpcs -i' to confirm that PHPCompatibility has been added to the list
of coding standards.
* Run 'phpcs --standard=PHPCompatibility <files>' to check <files>
--- PHP7 Compatibility hints ---
func_get_args() should be placed at the beginning of a method or function,
before any modifications to the function's arguments take place. Its value
should be assigned to a locally scoped variable - usually named $args by
convention - to be used inside the function as needed. This is due to
changes in PHP 7.0 and onwards.
@see http://php.net/manual/en/function.func-get-args.php#refsect1-function.func-get-args-notes
-- CONTACT --
Current maintainers:
* Francesco Placella (plach) - http://drupal.org/user/183211
* Daniel F. Kudwien (sun) - http://drupal.org/user/54136
* Stefanos Petrakis (stefanos.petrakis) - https://www.drupal.org/u/stefanospetrakis
@@ -13,11 +13,30 @@ function entity_translation_admin_form($form, $form_state) {
$form['locale_field_language_fallback'] = array(
'#type' => 'checkbox',
'#title' => t('Enable language fallback'),
'#description' => t('When language fallback is enabled, if a translation is not available for the requested language an existing one will be displayed.'),
'#title' => t('Allow language fallback'),
'#description' => t('When language fallback is allowed, an alternative translation will be displayed if the requested language is not available. Other modules, such as <a href="https://www.drupal.org/project/language_hierarchy" target="_blank">Language Hierarchy</a>, are needed to manage which languages are used as fallbacks. Otherwise, existing translations will be chosen based on their <a href="!url">language weight</a>.', array('!url' => url('admin/config/regional/language'))),
'#default_value' => variable_get('locale_field_language_fallback', TRUE),
);
$form['entity_translation_languages_enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Use only enabled languages'),
'#description' => t('If checked, disabled languages will not show up as available languages. This setting does not apply to content types that are configured to use the Multilingual content (i18n_node) module as this module provides its own configuration.'),
'#default_value' => variable_get('entity_translation_languages_enabled', FALSE),
);
$form['entity_translation_show_fallback_on_overview_pages'] = array(
'#type' => 'checkbox',
'#title' => t('Show fallback statuses on overview pages'),
'#description' => t('Enable to the show fallback information on the entity overview pages.'),
'#default_value' => variable_get('entity_translation_show_fallback_on_overview_pages', FALSE),
'#states' => array(
'visible' => array(
':input[name="locale_field_language_fallback"]' => array('checked' => TRUE),
),
),
);
$form['entity_translation_shared_labels'] = array(
'#type' => 'checkbox',
'#title' => t('Display shared labels'),
@@ -33,7 +52,7 @@ function entity_translation_admin_form($form, $form_state) {
);
$_null = NULL;
list($items, ) = menu_router_build();
list($items,) = menu_router_build();
_entity_translation_validate_path_schemes($_null, FALSE, $items);
foreach (entity_get_info() as $entity_type => $info) {
@@ -153,6 +172,46 @@ function entity_translation_admin_form($form, $form_state) {
}
}
if (module_exists('pathauto')) {
$form['pathauto'] = array(
'#type' => 'fieldset',
'#title' => t('Pathauto'),
'#collapsible' => FALSE,
);
$form['pathauto']['entity_translation_pathauto_state_active_new_translation'] = array(
'#title' => t('Pathauto state automatically activated on forms for new translations'),
'#type' => 'checkbox',
'#description' => t('When this option is enabled, the Pathauto state checkbox is automatically selected on the add-translation form. <br/><b>Attention:</b>When combining this option with the next one it may lead to cases where all aliases for all languages are auto-updated whenever a new translation is added.'),
'#default_value' => variable_get('entity_translation_pathauto_state_active_new_translation', TRUE),
);
// These are the possible options when pathauto state is active.
$mode_options = array(
'generate_alias_active_language' => t('Generate alias only for current language'),
'generate_missing_aliases_all_languages' => t('Generate missing aliases for all languages'),
'generate_all_aliases_all_languages' => t('Generate all aliases for all languages'),
);
// Single entity update mode.
$form['pathauto']['entity_translation_pathauto_state_mode_update'] = array(
'#title' => t('Pathauto state mode for single update'),
'#type' => 'select',
'#description' => t('When the Pathauto state is activated, the mode defined here will be used for alias generation, on a single update, e.g. when editing a node using the node edit form.'),
'#options' => $mode_options,
'#default_value' => variable_get('entity_translation_pathauto_state_mode_update', 'generate_alias_active_language'),
);
// Bulk entity updates mode.
$form['pathauto']['entity_translation_pathauto_state_mode_bulkupdate'] = array(
'#title' => t('Pathauto state mode for bulk updates'),
'#type' => 'select',
'#description' => t('When the Pathauto state is activated, the mode defined here will be used for alias generation, on bulk updates.'),
'#options' => $mode_options,
'#default_value' => variable_get('entity_translation_pathauto_state_mode_bulkupdate', 'generate_all_aliases_all_languages'),
);
}
$form = system_settings_form($form);
// Menu rebuilding needs to be performed after the system settings are saved.
@@ -198,12 +257,17 @@ function entity_translation_settings_init($entity_type, $settings = array()) {
* Translations overview page callback.
*/
function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$args = func_get_args();
// Entity translation and node translation share the same system path.
if ($callback && entity_translation_node($entity_type, $entity)) {
return entity_translation_overview_callback($callback, $entity);
if ($callback && !entity_translation_enabled($entity_type, $entity)) {
$callback_args = array_slice($args, 3);
return entity_translation_overview_callback($callback, $callback_args);
}
$handler = entity_translation_get_handler($entity_type, $entity);
// Ensure $entity holds an entity object and not an id.
$entity = $handler->getEntity();
$handler->initPathScheme();
// Initialize translations if they are empty.
@@ -217,8 +281,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// languages.
if ($handler->initOriginalTranslation()) {
// FIXME!
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
}
$header = array(t('Language'), t('Source language'), t('Translation'), t('Status'), t('Operations'));
@@ -234,6 +297,8 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$links = EntityTranslationDefaultHandler::languageSwitchLinks($path);
}
$show_fallback = variable_get('locale_field_language_fallback', TRUE) && variable_get('entity_translation_show_fallback_on_overview_pages', FALSE);
foreach ($languages as $language) {
$classes = array();
$options = array();
@@ -284,7 +349,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = t('n/a');
if ($source != $langcode && $handler->getAccess('update')) {
if ($source != $langcode && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$translatable = FALSE;
@@ -300,9 +365,21 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language);
$link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
$options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields');
$classes[] = $translatable ? '' : 'non-traslatable';
$classes[] = $translatable ? '' : 'non-translatable';
}
$status = t('Not translated');
// Show fallback information if required.
if ($show_fallback) {
$language_fallback_candidates = _entity_translation_language_fallback_get_candidates($language);
$fallback_candidates = array_intersect_key(drupal_map_assoc($language_fallback_candidates), $translations->data);
$fallback_langcode = reset($fallback_candidates);
if ($fallback_langcode !== FALSE) {
$status = t('Fallback from @language', array('@language' => $languages[$fallback_langcode]->name));
$label = _entity_translation_label($entity_type, $entity, $fallback_langcode);
$link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $langcode);
$row_title = l($label, $link['href'], $link);
}
}
}
$rows[] = array(
'data' => array($language_name, $source_name, $row_title, $status, implode(" | ", $options)),
@@ -329,13 +406,13 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
/**
* Calls the appropriate translation overview callback.
*/
function entity_translation_overview_callback($callback, $entity) {
function entity_translation_overview_callback($callback, $args) {
if (module_exists($callback['module'])) {
if (isset($callback['file'])) {
$path = isset($callback['file path']) ? $callback['file path'] : drupal_get_path('module', $callback['module']);
require_once DRUPAL_ROOT . '/' . $path . '/' . $callback['file'];
}
return $callback['page callback']($entity);
return call_user_func_array($callback['page callback'], $args);
}
}
@@ -361,7 +438,7 @@ function _entity_translation_label($entity_type, $entity, $langcode = NULL) {
/**
* Theme wrapper for the entity translation language page.
*/
function theme_entity_translation_overview($variables){
function theme_entity_translation_overview($variables) {
$rows = $variables['rows'];
return theme('table', array(
'rows' => $rows,
@@ -372,7 +449,7 @@ function theme_entity_translation_overview($variables){
/**
* Theme wrapper for the entity translation language outdated translation.
*/
function theme_entity_translation_overview_outdated($variables){
function theme_entity_translation_overview_outdated($variables) {
$message = $variables['message'];
return ' - <span class="marker">' . $message . '</span>';
}
@@ -382,12 +459,15 @@ function theme_entity_translation_overview_outdated($variables){
*/
function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->setActiveLanguage($langcode);
$handler->initPathScheme();
$languages = language_list();
$form = array(
'#handler' => $handler,
'#entity_type' => $entity_type,
'#entity' => $entity,
// Ensure $entity holds an entity object and not an id.
'#entity' => $handler->getEntity(),
'#language' => $langcode,
);
@@ -416,13 +496,12 @@ function entity_translation_delete_confirm_submit($form, &$form_state) {
// Remove the translation entry and the related fields.
$handler->removeTranslation($langcode);
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
$form_state['redirect'] = $handler->getTranslatePath();
}
/*
/**
* Confirm form for changing field translatability.
*/
function entity_translation_translatable_form($form, &$form_state, $field_name) {
@@ -517,7 +596,7 @@ function entity_translation_translatable_form_submit($form, $form_state) {
batch_set($batch);
}
/*
/**
* Toggle translatability of the given field.
*
* This is called from a batch operation, but should only run once per field.
@@ -548,6 +627,7 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
$query = new EntityFieldQuery();
$count = $query
->fieldCondition($field_name)
->age(FIELD_LOAD_REVISION)
->count()
->execute();
@@ -567,11 +647,32 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
$result = $query
->fieldCondition($field_name)
->entityOrderBy('entity_id')
->age(FIELD_LOAD_REVISION)
->range($offset, $limit)
->execute();
foreach ($result as $entity_type => $entities) {
foreach (entity_load($entity_type, array_keys($entities)) as $entity) {
foreach ($result as $entity_type => $partial_entities) {
// Load all revisionable entities' revisions.
if (EntityTranslationDefaultHandler::isEntityTypeRevisionable($entity_type)) {
$entities = array();
// Extract the revision identifier from the entity's definition.
$entity_info = entity_get_info($entity_type);
$revision_id_key = $entity_info['entity keys']['revision'];
// Load each revisionable entity's revision using $conditions, which
// should include the revision id information.
foreach ($partial_entities as $partial_entity) {
$conditions = (array) $partial_entity;
$revision_condition = array_intersect_key($conditions, array($revision_id_key => $revision_id_key));
$entity_revisions = entity_load($entity_type, FALSE, $revision_condition);
$entities[] = reset($entity_revisions);
}
}
else {
$entities = entity_load($entity_type, array_keys($partial_entities));
}
foreach ($entities as $entity) {
$context['sandbox']['progress']++;
$handler = entity_translation_get_handler($entity_type, $entity);
$langcode = $handler->getLanguage();
@@ -590,9 +691,10 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
if ($translatable && isset($entity->{$field_name}[LANGUAGE_NONE])) {
// If the field is being switched to translatable and has data for
// LANGUAGE_NONE then we need to move the data to the right language.
// In case the 'Copy translations' option was selected, move the
// available LANGUAGE_NONE field data into all existing translation
// languages, otherwise only into the entity's language.
$translations = $handler->getTranslations();
if ($copy_all_languages && !empty($translations->data)) {
foreach ($translations->data as $translation) {
$entity->{$field_name}[$translation['language']] = $entity->{$field_name}[LANGUAGE_NONE];
@@ -650,8 +752,7 @@ function _entity_translation_update_field($entity_type, $entity, $field_name) {
// otherwise any stored empty field value would be deleted. If this happens
// the range queries would be messed up.
if ($empty < count($entity->{$field_name})) {
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
}
}
@@ -667,3 +768,36 @@ function entity_translation_translatable_batch_done($success, $results, $operati
drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."));
}
}
/**
* Returns language fallback candidates for a certain language.
*
* @param object $language
* Drupal language object.
*
* @return array
* An array of language codes in the fallback order.
*/
function _entity_translation_language_fallback_get_candidates($language) {
// Save original fallback candidates.
$language_fallback_original = drupal_static('language_fallback_get_candidates');
// Replace all global languages with the given one. We need this because the
// language_fallback_get_candidates() does not take the $language parameter,
// however other modules (like language_fallback) may use current language(s)
// when they alter the language fallback candidates.
$languages_original = array();
foreach (language_types() as $language_type) {
$languages_original[$language_type] = $GLOBALS[$language_type];
$GLOBALS[$language_type] = $language;
}
// Clear static cache, so that fallback candidates are recalculated.
drupal_static_reset('language_fallback_get_candidates');
$language_fallback_candidates = language_fallback_get_candidates();
// Restore original data.
$language_fallback =& drupal_static('language_fallback_get_candidates');
$language_fallback = $language_fallback_original;
foreach ($languages_original as $language_type => $_language) {
$GLOBALS[$language_type] = $_language;
}
return $language_fallback_candidates;
}
@@ -64,7 +64,7 @@
* - skip original values access: A flag specifying whether skipping access
* control when editing original values for this entity. Defaults to FALSE.
* - bundle callback: A callback to check whether the passed bundle has entity
* translation enabled. If empty all bundles are supposed to be enabled.
* translation enabled. Defaults to TRUE for all bundles.
* - default settings: The defaults to be applied to settings when an explicit
* choice is missing.
*/
@@ -91,7 +91,7 @@ function hook_entity_info() {
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationCustomEntityHandler',
'path sets' => array(
'path schemes' => array(
'default' => array(
'base path' => 'custom_entity_2/%custom_entity',
'path wildcard' => '%custom_entity',
@@ -151,3 +151,30 @@ function hook_entity_translation_update($entity_type, $entity, $translation, $va
*/
function hook_entity_translation_delete($entity_type, $entity, $langcode) {
}
/**
* Allows modules to act when a revision translation is deleted.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity.
* @param $langcode
* The langcode of the revision translation which was deleted.
*/
function hook_entity_translation_delete_revision($entity_type, $entity, $langcode) {
}
/**
* Allows to sets the right values in the form state when adding a translation.
*/
function hook_entity_translation_source_field_state_alter(&$field_state) {
if (isset($field_state['entity'])) {
module_load_include('inc', 'entity', 'includes/entity.ui');
foreach ($field_state['entity'] as $delta => $entity) {
if ($entity instanceof FieldCollectionItemEntity) {
$field_state['entity'][$delta] = entity_ui_clone_entity('field_collection_item', $entity);
}
}
}
}
@@ -5,11 +5,18 @@ core = 7.x
configure = admin/config/regional/entity_translation
dependencies[] = locale (>7.14)
test_dependencies[] = pathauto:pathauto
; The version restriction in the following line could be
; removed as soon as [#3085061] is fixed.
test_dependencies[] = title:title (=7.x-1.0-alpha9)
files[] = includes/translation.handler_factory.inc
files[] = includes/translation.handler.inc
files[] = includes/translation.handler.comment.inc
files[] = includes/translation.handler.node.inc
files[] = includes/translation.handler.taxonomy_term.inc
files[] = includes/translation.handler.user.inc
files[] = includes/translation.migrate.inc
files[] = tests/entity_translation.test
@@ -19,10 +26,10 @@ files[] = views/entity_translation_handler_field_label.inc
files[] = views/entity_translation_handler_filter_entity_type.inc
files[] = views/entity_translation_handler_filter_language.inc
files[] = views/entity_translation_handler_filter_translation_exists.inc
files[] = views/entity_translation_handler_field_field.inc
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -9,6 +9,8 @@
* Implements hook_schema().
*/
function entity_translation_schema() {
$schema = array();
$schema['entity_translation'] = array(
'description' => 'Table to track entity translations',
'fields' => array(
@@ -25,7 +27,12 @@ function entity_translation_schema() {
'not null' => TRUE,
'description' => 'The entity id this translation relates to',
),
// @todo: Consider an integer field for 'language'.
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
@@ -73,6 +80,78 @@ function entity_translation_schema() {
),
'primary key' => array('entity_type', 'entity_id', 'language'),
);
$schema['entity_translation_revision'] = array(
'description' => 'Table to track entity translation revisions',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation revision relates to',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this translation revision relates to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation revision.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation revision was created.',
),
'uid' => array(
'description' => 'The author of this translation revision.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'translate' => array(
'description' => 'A boolean indicating whether this translation revision needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'The Unix timestamp when the translation revision was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation revision was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'revision_id', 'language'),
'indexes'=> array('revision_id' => array('revision_id')),
);
return $schema;
}
@@ -93,6 +172,12 @@ function entity_translation_install() {
// Make translation use the content language type.
variable_set('translation_language_type', LANGUAGE_TYPE_CONTENT);
// Enable revision support for entity translation.
variable_set('entity_translation_revision_enabled', TRUE);
// Enable taxonomy autocomplete support.
variable_set('entity_translation_taxonomy_autocomplete', TRUE);
}
/**
@@ -254,3 +339,162 @@ function entity_translation_update_7003() {
function entity_translation_update_7004() {
entity_info_cache_clear();
}
/**
* Rebuild the class registry to pick up the translation handler factory class.
*/
function entity_translation_update_7005() {
registry_rebuild();
}
/**
* Add revision schema for entity translation metadata.
*/
function entity_translation_update_7006() {
// Create revision id column.
$spec = array(
'type' => 'int',
'unsigned' => TRUE,
// If we have existing data we cannot enforce this to be NOT NULL.
'not null' => FALSE,
'description' => 'The entity revision id this translation relates to',
);
// Create revision id column if it doesn't exist already.
if (!db_field_exists('entity_translation', 'revision_id')) {
db_add_field('entity_translation', 'revision_id', $spec);
}
// Create the entity translation revision schema.
$table = array(
'description' => 'Table to track entity translation revisions',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation revision relates to',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this translation revision relates to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation revision.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation revision was created.',
),
'uid' => array(
'description' => 'The author of this translation revision.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'translate' => array(
'description' => 'A boolean indicating whether this translation revision needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'The Unix timestamp when the translation revision was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation revision was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'revision_id', 'language'),
'indexes'=> array('revision_id' => array('revision_id')),
);
// Create entity translation revision table if it doesn't exist already.
if (!db_table_exists('entity_translation_revision')) {
db_create_table('entity_translation_revision', $table);
}
}
/**
* Disable revision support on existing installations.
*/
function entity_translation_update_7007() {
// Revision support is not enabled by default on existing installations as
// making it work implies copying translation metadata to the
// {entity_translation_revision} table for all the existing translations.
// Since this process cannot be reliably implemented in an update function,
// we leave the choice of manually performing the upgrade to people with the
// required skills to do so. Be aware that enabling revision support on sites
// where data has not been manually migrated may cause translation metadata to
// be permanently lost or corrupted. See https://www.drupal.org/node/2396103.
variable_set('entity_translation_revision_enabled', FALSE);
}
/**
* Enable Entity Translation for taxonomy vocabularies having terms translated
* through it.
*/
function entity_translation_update_7008() {
if (!module_exists('taxonomy')) {
return;
}
// According to EXPLAIN joining {taxonomy_vocabulary} here makes the query
// perform way worse, so we just split into two quick ones.
$query = "SELECT t.vid
FROM {entity_translation} et
JOIN {taxonomy_term_data} t ON et.entity_type = 'taxonomy_term' AND et.entity_id = t.tid AND et.source != ''
GROUP BY t.vid";
$vids = db_query($query)->fetchCol();
if ($vids) {
$query = "SELECT v.machine_name FROM {taxonomy_vocabulary} v WHERE v.vid IN (:vids)";
$names = db_query($query, array(':vids' => $vids))->fetchCol();
$info = variable_get('entity_translation_taxonomy', array());
foreach ($names as $name) {
$info[$name] = TRUE;
}
variable_set('entity_translation_taxonomy', $info);
}
}
/**
* Make sure "i18n_mode" is correctly set for vocabularies having entity
* translation enabled.
*/
function entity_translation_update_7009() {
$info = array_filter(variable_get('entity_translation_taxonomy', array()));
if ($info && module_exists('i18n_taxonomy')) {
$query = "UPDATE {taxonomy_vocabulary} SET i18n_mode = :et_mode WHERE machine_name IN (:names)";
$names = array_keys(array_filter($info));
db_query($query, array(':et_mode' => 32768, ':names' => $names));
}
}
@@ -0,0 +1,17 @@
<?php
/**
* @file
* Implements a Migrate destination handler to automatically add translations
* to entities with translatable fields.
*/
/**
* Implements hook_migrate_api().
*/
function entity_translation_migrate_api() {
$api = array(
'api' => 2,
'destination handlers' => array('MigrateTranslationEntityHandler'),
);
return $api;
}
@@ -6,6 +6,7 @@
*/
module_load_include('inc', 'entity_translation', 'entity_translation.node');
module_load_include('inc', 'entity_translation', 'entity_translation.taxonomy');
/**
@@ -23,6 +24,11 @@ define('ENTITY_TRANSLATION_LANGUAGE_CURRENT', 'xx-et-current');
*/
define('ENTITY_TRANSLATION_LANGUAGE_AUTHOR', 'xx-et-author');
/**
* Defines an i18n translation mode for Entity Translation.
*/
define('I18N_MODE_ENTITY_TRANSLATION', 32768);
/**
* Implements hook_hook_info().
@@ -105,8 +111,11 @@ function entity_translation_entity_info() {
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationTaxonomyTermHandler',
'access callback' => 'entity_translation_taxonomy_term_tab_access',
'access arguments' => array(1),
'base path' => 'taxonomy/term/%taxonomy_term',
'edit form' => 'term',
'bundle callback' => 'entity_translation_taxonomy_term_enabled_vocabulary',
),
),
);
@@ -129,7 +138,7 @@ function entity_translation_entity_info() {
* Processes the given path schemes and fill-in default values.
*/
function _entity_translation_process_path_schemes($entity_type, &$et_info) {
$path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme'));
$path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme', 'edit tabs'));
// Insert the default path scheme into the 'path schemes' array and remove
// respective elements from the entity_translation info array.
@@ -164,6 +173,7 @@ function _entity_translation_process_path_schemes($entity_type, &$et_info) {
$et_info['path schemes'][$delta] += array(
'admin theme' => TRUE,
'path wildcard' => "%$entity_type",
'edit tabs' => TRUE,
);
}
}
@@ -184,6 +194,9 @@ function entity_translation_entity_info_alter(&$entity_info) {
// not. As a matter of fact we might need them to correctly switch field
// translatability when a field is shared across different entity types.
$et_info += array('class' => 'EntityTranslationDefaultHandler');
if (!isset($entity_info[$entity_type]['entity keys'])) {
$entity_info[$entity_type]['entity keys'] = array();
}
$entity_info[$entity_type]['entity keys'] += array('translations' => 'translations');
if (entity_translation_enabled($entity_type, NULL, TRUE)) {
@@ -231,6 +244,13 @@ function entity_translation_menu() {
'file' => 'entity_translation.admin.inc',
);
$items['entity_translation/taxonomy_term/autocomplete'] = array(
'title' => 'Entity translation autocomplete',
'page callback' => 'entity_translation_taxonomy_term_autocomplete',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
@@ -286,7 +306,7 @@ function _entity_translation_validate_path_schemes(&$schemes, $entity_type_label
if (empty($scheme['path wildcard'])) {
if ($warnings) {
$t_args = array('%scheme' => $delta, '%entity_type' => $entity_type_label);
drupal_set_message(t('Entity Translation path scheme %scheme for entities of type %entity_type does not declare a path wildcard.', $t_args), 'warning');
watchdog('entity_translation', 'Entity Translation path scheme %scheme for entities of type %entity_type does not declare a path wildcard.', $t_args);
}
unset($schemes[$delta]);
continue;
@@ -444,7 +464,11 @@ function entity_translation_menu_alter(&$items) {
// Replace the main edit callback with our proxy implementation to set
// form language to the current language and check access.
$entity_position = array_search($scheme['path wildcard'], $edit_path_parts);
$original_item = $edit_item;
// Make sure incoming page and access arguments are arrays.
$original_item = $edit_item + array(
'page arguments' => array(),
'access arguments' => array(),
);
$args = array($entity_type, $entity_position, FALSE, $original_item);
$edit_item['page callback'] = 'entity_translation_edit_page';
$edit_item['page arguments'] = array_merge($args, $original_item['page arguments']);
@@ -452,21 +476,23 @@ function entity_translation_menu_alter(&$items) {
$edit_item['access arguments'] = array_merge($args, $original_item['access arguments']);
// Edit translation callback.
$translation_position = count($edit_path_parts);
$args = array($entity_type, $entity_position, $translation_position, $original_item);
$items["$edit_path/%entity_translation_language"] = array(
'type' => MENU_DEFAULT_LOCAL_TASK,
'title callback' => 'entity_translation_edit_title',
'title arguments' => array($translation_position),
'page callback' => 'entity_translation_edit_page',
'page arguments' => array_merge($args, $original_item['page arguments']),
'access callback' => 'entity_translation_edit_access',
'access arguments' => array_merge($args, $original_item['access arguments']),
)
// We need to inherit the remaining menu item keys, mostly 'module'
// and 'file' to keep ajax callbacks working (see form_get_cache() and
// drupal_retrieve_form()).
+ $original_item;
if ($scheme['edit tabs'] !== FALSE) {
$translation_position = count($edit_path_parts);
$args = array($entity_type, $entity_position, $translation_position, $original_item);
$items["$edit_path/%entity_translation_language"] = array(
'type' => MENU_DEFAULT_LOCAL_TASK,
'title callback' => 'entity_translation_edit_title',
'title arguments' => array($translation_position),
'page callback' => 'entity_translation_edit_page',
'page arguments' => array_merge($args, $original_item['page arguments']),
'access callback' => 'entity_translation_edit_access',
'access arguments' => array_merge($args, $original_item['access arguments']),
)
// We need to inherit the remaining menu item keys, mostly 'module'
// and 'file' to keep ajax callbacks working (see form_get_cache() and
// drupal_retrieve_form()).
+ $original_item;
}
// Add translation callback.
$add_path = "$edit_path/add/%entity_translation_language/%entity_translation_language";
@@ -498,12 +524,13 @@ function entity_translation_menu_alter(&$items) {
}
if ($translate_ui_attached == FALSE) {
drupal_set_message(t('The entities of type %entity_type do not define a valid path scheme: it will not be possible to translate them.', array('%entity_type' => $info['label'])), 'warning');
watchdog('entity_translation', 'The entities of type %entity_type do not define a valid path scheme: it will not be possible to translate them.', array('%entity_type' => $info['label']));
}
// Node-specific menu alterations.
if ($entity_type == 'node') {
entity_translation_node_menu_alter($items, $backup);
// Entity-type-specific menu alterations.
$function = 'entity_translation_' . $entity_type . '_menu_alter';
if (function_exists($function)) {
$function($items, $backup);
}
}
}
@@ -533,8 +560,8 @@ function entity_translation_edit_page() {
// Set the current form language.
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
$langcode = entity_translation_form_language($langcode, $handler);
$handler->setFormLanguage($langcode);
$langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
$handler->setActiveLanguage($langcode);
// Display the entity edit form.
return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item);
@@ -548,10 +575,20 @@ function entity_translation_edit_access() {
$entity_type = array_shift($args);
$entity = array_shift($args);
$langcode = array_shift($args);
$edit_form_item = array_shift($args);
$access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
$handler = entity_translation_get_handler($entity_type, $entity);
// First, check a handler has been loaded. This could be empty if a
// non-existent entity edit path has been requested, for example. Delegate
// directly to the edit form item access callback in this case.
if (empty($handler)) {
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
$translations = $handler->getTranslations();
$langcode = entity_translation_form_language($langcode, $handler);
$langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
// The user must be explicitly allowed to access the original values if
// workflow permissions are enabled.
@@ -567,8 +604,7 @@ function entity_translation_edit_access() {
// is language neutral we need to let editors access it.
$enabled_languages = entity_translation_languages($entity_type, $entity);
if (isset($enabled_languages[$langcode]) || $langcode == LANGUAGE_NONE) {
$edit_form_item = array_shift($args);
return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item);
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
}
@@ -578,11 +614,6 @@ function entity_translation_edit_access() {
/**
* Determines the current form language.
*
* Based on the requested language and the translations available for the entity
* being edited, determines the active form language. This takes into account
* language fallback rules so that the translation being edited matches the one
* being viewed.
*
* @param $langcode
* The requested language code.
* @param EntityTranslationHandlerInterface $handler
@@ -590,8 +621,35 @@ function entity_translation_edit_access() {
*
* @return
* A valid language code.
*
* @deprecated This is no longer used and will be removed in the first stable
* release.
*/
function entity_translation_form_language($langcode, $handler) {
return entity_translation_get_existing_language($handler->getEntity(), $handler->getEntityType(), $langcode);
}
/**
* Determines an existing translation language.
*
* Based on the requested language and the translations available for the given
* entity, determines an existing translation language. This takes into account
* language fallback rules.
*
* @param $entity_type
* The type of the entity.
* @param $entity
* The entity whose existing translation language has to be returned.
* @param $langcode
* (optional) The requested language code. Defaults to the current content
* language.
*
* @return
* A valid language code.
*/
function entity_translation_get_existing_language($entity_type, $entity, $langcode = NULL) {
$handler = entity_translation_get_handler($entity_type, $entity);
if (empty($langcode)) {
$langcode = $GLOBALS['language_content']->language;
}
@@ -625,7 +683,8 @@ function entity_translation_add_access() {
$enabled_languages = entity_translation_languages($entity_type, $entity);
if (isset($enabled_languages[$langcode])) {
$edit_form_item = array_shift($args);
return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item);
$access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
}
@@ -645,7 +704,7 @@ function entity_translation_add_page() {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
$handler->setFormLanguage($langcode);
$handler->setActiveLanguage($langcode);
$handler->setSourceLanguage($source);
// Display the entity edit form.
@@ -669,7 +728,7 @@ function _entity_translation_callback($callback, $args, $info = array()) {
function entity_translation_admin_paths() {
$paths = array();
foreach (entity_get_info() as $entity_type => $info) {
if (entity_translation_enabled($entity_type, NULL, TRUE)) {
if (isset($info['translation']['entity_translation']['path schemes']) && entity_translation_enabled($entity_type, NULL, TRUE)) {
foreach ($info['translation']['entity_translation']['path schemes'] as $scheme) {
if (!empty($scheme['admin theme'])) {
if (isset($scheme['translate path'])) {
@@ -693,8 +752,12 @@ function entity_translation_admin_paths() {
*/
function entity_translation_tab_access($entity_type, $entity) {
if (drupal_multilingual() && (user_access('translate any entity') || user_access("translate $entity_type entities"))) {
$handler = entity_translation_get_handler($entity_type, $entity);
// Ensure $entity holds an entity object and not an id.
$entity = $handler->getEntity();
$enabled = entity_translation_enabled($entity_type, $entity);
return $enabled && entity_translation_get_handler($entity_type, $entity)->getLanguage() != LANGUAGE_NONE;
return $enabled && $handler->getLanguage() != LANGUAGE_NONE;
}
return FALSE;
}
@@ -866,7 +929,7 @@ function entity_translation_field_language_alter(&$display_language, $context) {
if (isset($translations->data[$context['language']]) && !entity_translation_access($entity_type, $translations->data[$context['language']])) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instances = field_info_instances($entity_type, $bundle);
$entity = clone($entity);
$entity = clone $entity;
foreach ($translations->data as $langcode => $translation) {
if ($langcode == $context['language'] || !entity_translation_access($entity_type, $translations->data[$langcode])) {
@@ -1058,6 +1121,10 @@ function entity_translation_sync($entity_type, $entity) {
// If an item has been removed we do not store its translations.
if ($removed) {
// Ensure items are actually removed.
if (!isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = array();
}
continue;
}
// If a synchronized column has changed we need to override the full
@@ -1118,14 +1185,27 @@ function entity_translation_field_attach_delete($entity_type, $entity) {
}
}
/**
* Implements hook_field_attach_delete_revision().
*/
function entity_translation_field_attach_delete_revision($entity_type, $entity) {
if (entity_translation_enabled($entity_type, $entity)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->removeRevisionTranslations();
$handler->saveTranslations();
}
}
/**
* Implementation of hook_field_attach_form().
*/
function entity_translation_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
// Skip recursing into the source form.
if (empty($form['#entity_translation_source_form']) && ($handler = entity_translation_entity_form_get_handler($form, $form_state))) {
// Avoid recursing into the source form.
list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
if (empty($form['#entity_translation_source_form']) && entity_translation_enabled($entity_type, $bundle)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$langcode = !empty($langcode) ? $langcode : $handler->getLanguage();
$form_langcode = $handler->getFormLanguage();
$form_langcode = $handler->getActiveLanguage();
$translations = $handler->getTranslations();
$update_langcode = $form_langcode && ($form_langcode != $langcode);
$source = $handler->getSourceLanguage();
@@ -1140,8 +1220,6 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// with the correct form language and replace the field elements with the
// correct ones.
if ($update_langcode || ($source && !isset($translations->data[$form_langcode]) && isset($translations->data[$source]) && empty($form_state['rebuild']))) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
@@ -1150,26 +1228,31 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// language information from source to target language, this way the
// user can find the form items already populated with the source values
// while the field form element holds the correct language information.
if ($field['translatable']) {
$form[$field_name]['#field_name'] = $field_name;
$form[$field_name]['#source'] = $update_langcode ? $form_langcode : $source;
$form[$field_name]['#previous'] = NULL;
if ($field['translatable'] && isset($form[$field_name])) {
$element = &$form[$field_name];
$element['#entity_type'] = $entity_type;
$element['#entity'] = $entity;
$element['#entity_id'] = $id;
$element['#field_name'] = $field_name;
$element['#source'] = $update_langcode ? $form_langcode : $source;
$element['#previous'] = NULL;
$element['#form_parents'] = $form['#parents'];
// If we are updating the form language we need to make sure that the
// wrong language is unset and the right one is stored in the field
// element (see entity_translation_prepare_element()).
if ($update_langcode) {
$form[$field_name]['#previous'] = $form[$field_name]['#language'];
$form[$field_name]['#language'] = $form_langcode;
$element['#previous'] = $element['#language'];
$element['#language'] = $form_langcode;
}
// Swap default values during form processing to avoid recursion. We
// try to act before any other callback so that the correct values are
// already in place for them.
if (!isset($form[$field_name]['#process'])) {
$form[$field_name]['#process'] = array();
if (!isset($element['#process'])) {
$element['#process'] = array();
}
array_unshift($form[$field_name]['#process'], 'entity_translation_prepare_element');
array_unshift($element['#process'], 'entity_translation_prepare_element');
}
}
}
@@ -1181,20 +1264,23 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
// If access is not set or is granted we check whether the user has
// access to shared fields.
$form[$field_name]['#access'] = (!isset($form[$field_name]['#access']) || $form[$field_name]['#access']) && ($field['translatable'] || $shared_access);
$form[$field_name]['#multilingual'] = (boolean) $field['translatable'];
// Check if a field is part of the form array.
if (isset($form[$field_name])) {
$field = field_info_field($field_name);
// If access is not set or is granted we check whether the user has
// access to shared fields.
$form[$field_name]['#access'] = (!isset($form[$field_name]['#access']) || $form[$field_name]['#access']) && ($field['translatable'] || $shared_access);
$form[$field_name]['#multilingual'] = (boolean) $field['translatable'];
}
}
}
// If a translation is being created and no path alias exists for its
// language, by default an alias needs to be generated. The standard
// behavior is defaulting to FALSE when an entity already exists, hence we
// need to override it here.
// If a translation is being created, an alias may be generated if needed.
// The standard behavior is defaulting to FALSE when an entity already
// exists, hence we need to override it here.
// If needed, a variable is provided for reverting to the default behavior.
if (module_exists('pathauto') && $handler->getSourceLanguage()) {
$entity->path['pathauto'] = TRUE;
$entity->path['pathauto'] = variable_get('entity_translation_pathauto_state_active_new_translation', TRUE);
}
}
}
@@ -1203,18 +1289,37 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
* Form element process callback.
*/
function entity_translation_prepare_element($element, &$form_state) {
$source_form = &drupal_static(__FUNCTION__, array());
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast = &drupal_static(__FUNCTION__, array(
'source_forms' => array(),
'source_form_states' => array(),
));
}
$source_forms = &$drupal_static_fast['source_forms'];
$source_form_states = &$drupal_static_fast['source_form_states'];
$form = $form_state['complete form'];
$build_id = $form['#build_id'];
$source = $element['#source'];
$entity_type = $element['#entity_type'];
$id = $element['#entity_id'];
if (!isset($source_form[$build_id][$source])) {
$source_form[$build_id][$source] = array('#entity_translation_source_form' => TRUE);
// Key the source form cache per entity type and entity id to allow for
// multiple entities on the same entity form.
if (!isset($source_forms[$build_id][$source][$entity_type][$id])) {
$source_form = array(
'#entity_translation_source_form' => TRUE,
'#parents' => $element['#form_parents'],
);
$source_form_state = $form_state;
$info = entity_translation_edit_form_info($form, $form_state);
field_attach_form($info['entity type'], $info['entity'], $source_form[$build_id][$source], $source_form_state, $source);
field_attach_form($entity_type, $element['#entity'], $source_form, $source_form_state, $source);
$source_forms[$build_id][$source][$entity_type][$id] = &$source_form;
$source_form_states[$build_id][$source][$entity_type][$id] = &$source_form_state;
}
$source_form = &$source_forms[$build_id][$source][$entity_type][$id];
$source_form_state = $source_form_states[$build_id][$source][$entity_type][$id];
$langcode = $element['#language'];
$field_name = $element['#field_name'];
@@ -1222,15 +1327,45 @@ function entity_translation_prepare_element($element, &$form_state) {
// language information from source to target language, this way the user can
// find the form items already populated with the source values while the
// field form element holds the correct language information.
if (isset($source_form[$build_id][$source][$field_name][$source])) {
$element[$langcode] = $source_form[$build_id][$source][$field_name][$source];
if (isset($source_form[$field_name][$source])) {
$element[$langcode] = $source_form[$field_name][$source];
entity_translation_form_element_language_replace($element, $source, $langcode);
entity_translation_form_element_state_replace($element, $source_form[$field_name], $field_name, $source_form_state, $form_state);
unset($element[$element['#previous']]);
}
return $element;
}
/**
* Helper function. Sets the right values in $form_state['field'] when using
* source language values as defaults.
*/
function entity_translation_form_element_state_replace($element, $source_element, $field_name, $source_form_state, &$form_state) {
if (isset($source_element['#language'])) {
$source = $source_element['#language'];
// Iterate through the form structure recursively.
foreach (element_children($element) as $key) {
if (isset($source_element[$key])) {
entity_translation_form_element_state_replace($element[$key], $source_element[$key], $key, $source_form_state, $form_state);
}
elseif (isset($source_element[$source])) {
entity_translation_form_element_state_replace($element[$key], $source_element[$source], $key, $source_form_state, $form_state);
}
}
if (isset($source_element[$source]['#field_parents'])) {
$source_parents = $source_element[$source]['#field_parents'];
$langcode = $element['#language'];
$parents = $element[$langcode]['#field_parents'];
$source_state = field_form_get_state($source_parents, $field_name, $source, $source_form_state);
drupal_alter('entity_translation_source_field_state', $source_state);
field_form_set_state($parents, $field_name, $langcode, $form_state, $source_state);
}
}
}
/**
* Helper function. Recursively replaces the source language with the given one.
*/
@@ -1243,7 +1378,7 @@ function entity_translation_form_element_language_replace(&$element, $source, $l
// Replace specific occurrences of the source language with the target
// language.
foreach (element_properties($element) as $key) {
if ($key === '#language') {
if ($key === '#language' && $element[$key] != LANGUAGE_NONE) {
$element[$key] = $langcode;
}
elseif ($key === '#parents' || $key === '#field_parents') {
@@ -1343,6 +1478,10 @@ function _entity_translation_element_title_append(&$element, $suffix) {
if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
$element['#title'] .= $suffix;
}
// If this is a multi-valued field, apply the suffix to the container.
elseif (isset($element['#title']) && isset($element['#cardinality']) && $element['#cardinality'] != 1) {
$element['#title'] .= $suffix;
}
// If the current element does not have a (valid) title, try child elements.
elseif ($children = element_children($element)) {
foreach ($children as $delta) {
@@ -1360,30 +1499,32 @@ function _entity_translation_element_title_append(&$element, $suffix) {
* Implements hook_form_alter().
*/
function entity_translation_form_alter(&$form, &$form_state) {
if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
if (!$handler->isNewEntity()) {
$handler->entityForm($form, $form_state);
$translations = $handler->getTranslations();
$form_langcode = $handler->getFormLanguage();
if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
// Hide shared form elements if the user is not allowed to edit them.
$handler->entityFormSharedElements($form);
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
if (!$handler->isNewEntity()) {
$handler->entityForm($form, $form_state);
$translations = $handler->getTranslations();
$form_langcode = $handler->getActiveLanguage();
if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
// Hide shared form elements if the user is not allowed to edit them.
$handler->entityFormSharedElements($form);
}
}
else {
$handler->entityFormLanguageWidget($form, $form_state);
}
// We need to process the posted form as early as possible to update the
// form language value.
array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
}
// We might have an entity form for an entity or a bundle not enabled for
// translation. In this case we might need to deal with entity and field
// languages anyway, since fields may be shared among different bundles and
// entity types.
else {
$handler->entityFormLanguageWidget($form, $form_state);
}
// We need to process the posted form as early as possible to update the
// form language value.
array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
}
// We might have an entity form for an entity or a bundle not enabled for
// translation. In this case we might need to deal with entity and field
// languages anyway, since fields may be shared among different bundles and
// entity types.
elseif ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
$handler->entityFormLanguageWidget($form, $form_state);
}
}
@@ -1393,7 +1534,7 @@ function entity_translation_form_alter(&$form, &$form_state) {
function entity_translation_entity_form_source_language_submit($form, &$form_state) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$langcode = $form_state['values']['source_language']['language'];
$path = "{$handler->getEditPath()}/add/$langcode/{$handler->getFormLanguage()}";
$path = "{$handler->getEditPath()}/add/$langcode/{$handler->getActiveLanguage()}";
$options = array();
if (isset($_GET['destination'])) {
$options['query'] = drupal_get_destination();
@@ -1409,7 +1550,7 @@ function entity_translation_entity_form_source_language_submit($form, &$form_sta
*/
function entity_translation_entity_form_delete_translation_submit($form, &$form_state) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$path = "{$handler->getTranslatePath()}/delete/{$handler->getFormLanguage()}";
$path = "{$handler->getTranslatePath()}/delete/{$handler->getActiveLanguage()}";
$options = array();
if (isset($_GET['destination'])) {
$options['query'] = drupal_get_destination();
@@ -1429,16 +1570,22 @@ function entity_translation_entity_form_validate($form, &$form_state) {
}
/**
* Submit handler for the entity language widget.
* Validation handler for the entity language widget.
*/
function entity_translation_language_widget_submit($form, &$form_state) {
function entity_translation_entity_form_language_update($element, &$form_state, $form) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
// On non-translatable entities, we need to handle just the entity and field
// language.
if (empty($handler) && ($info = entity_translation_edit_form_info($form, $form_state))) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
// Ensure the handler form language match the actual one. This is mainly
// needed when responding to an AJAX request where the languages cannot be set
// from the usual page callback.
if (!empty($form_state['entity_translation']['form_langcode'])) {
$handler->setActiveLanguage($form_state['entity_translation']['form_langcode']);
}
// When responding to an AJAX request we should ignore any change in the
// language widget as it may alter the field language expected by the AJAX
// callback.
if (empty($form_state['triggering_element']['#ajax'])) {
$handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
$handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
/**
@@ -1460,10 +1607,13 @@ function entity_translation_entity_form_submit($form, &$form_state) {
* Mark translations as outdated if the submitted value is true.
*/
function entity_translation_field_attach_submit($entity_type, $entity, $form, &$form_state) {
if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
if (($handler = entity_translation_entity_form_get_handler($form, $form_state)) && entity_translation_enabled($entity_type, $entity)) {
// Update the wrapped entity with the submitted values.
$handler->setEntity($entity);
$handler->entityFormSubmit($form, $form_state);
// Process in-place translations for the taxonomy autocomplete widget.
entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, $form_state);
}
}
@@ -1538,7 +1688,7 @@ function theme_entity_translation_language_tabs($variables) {
* Adds an option to enable field synchronization.
* Enable a selector to choose whether a field is translatable.
*/
function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_state) {
function entity_translation_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
$instance = $form['#instance'];
$entity_type = $instance['entity_type'];
$field_name = $instance['field_name'];
@@ -1558,7 +1708,8 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st
$label = t('Field translation');
$title = t('Users may translate all occurrences of this field:') . _entity_translation_field_desc($field);
if (field_has_data($field)) {
$form_state['field_has_data'] = field_has_data($field);
if ($form_state['field_has_data']) {
$path = "admin/config/regional/entity_translation/translatable/$field_name";
$status = $translatable ? $title : (t('All occurrences of this field are untranslatable:') . _entity_translation_field_desc($field));
$link_title = !$translatable ? t('Enable translation') : t('Disable translation');
@@ -1586,6 +1737,11 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st
'#default_value' => $translatable,
);
}
$function = 'entity_translation_form_field_ui_field_edit_' . $instance['widget']['type'] . '_form_alter';
if (function_exists($function)) {
$function($form, $form_state);
}
}
/**
@@ -1646,6 +1802,9 @@ function entity_translation_enabled($entity_type, $entity = NULL, $skip_handler
/**
* Determines whether the given entity bundle is translatable.
*
* NOTE: Does not check for whether the entity type is translatable.
* Consider using entity_translation_enabled() instead.
*
* @param $entity_type
* The entity type the bundle to be checked belongs to.
* @param $bundle
@@ -1685,54 +1844,37 @@ function entity_translation_settings($entity_type, $bundle) {
* Entity language callback.
*
* This callback changes the entity language from the actual one to the active
* form language. This overriding allows to obtain language dependent form
* widgets where multilingual values are supported (e.g. field or path alias
* widgets) even if the code was not originally written with supporting multiple
* values per language in mind.
* language. This overriding allows to obtain language dependent form widgets
* where multilingual values are supported (e.g. field or path alias widgets)
* even if the code was not originally written with supporting multiple values
* per language in mind.
*
* The main drawback of this approach is that code needing to access the actual
* language in the entity form build/validation/submit workflow cannot rely on
* the entity_language() function. On the other hand in these scenarios assuming
* the presence of Entity translation should be safe, thus being able to rely on
* the EntityTranslationHandlerInterface::getLanguage() method.
* the presence of Entity translation should be safe, thus developers can rely
* on the EntityTranslationHandlerInterface::getLanguage() method.
*
* @param $entity_type
* @param string $entity_type
* The the type of the entity.
* @param $entity
* @param object $entity
* The entity whose language has to be returned.
*
* @return
* @return string
* A valid language code.
*/
function entity_translation_language($entity_type, $entity) {
// If a form has been post, we need to check its state to verify if any form
// translation handler is stored there. This is mainly needed when responding
// to an AJAX request where the form language cannot be set from the page
// callback.
$handler = entity_translation_current_form_get_handler();
// Make sure we always have a translation handler instance available.
if (empty($handler)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler = entity_translation_get_handler($entity_type, $entity);
if (!$handler) {
return LANGUAGE_NONE;
}
// If a translation handler associated to the current form is found, we need
// to update the wrapped entity. This way submitted values will be picked up.
// Other entities may be created or saved while submitting the current one,
// hence we need to check we are dealing with it.
elseif ($handler->isWrappedEntity($entity_type, $entity)) {
$langcode = $handler->getLanguage();
$handler->setEntity($entity);
$submitted_langcode = $handler->getLanguage();
// If the entity language has changed we are editing the original values. In
// this case we need to update the current form language with the submitted
// one.
if ($submitted_langcode != $langcode) {
$handler->setFormLanguage($submitted_langcode);
}
if (entity_translation_enabled($entity_type, $entity)) {
$langcode = $handler->getActiveLanguage();
return $langcode ? $langcode : $handler->getLanguage();
}
else {
return $handler->getLanguage();
}
$langcode = $handler->getFormLanguage();
return !empty($langcode) ? $langcode : $handler->getLanguage();
}
/**
@@ -1743,61 +1885,13 @@ function entity_translation_language($entity_type, $entity) {
* @param $entity
* (optional) The entity to be translated. A bundle name may be passed to
* instantiate an empty entity.
* @param $update
* (optional) Instances are statically cached: if this is TRUE the wrapped
* entity will be replaced by the passed one.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $update = FALSE) {
static $drupal_static_fast;
if (!isset($drupal_static_fast['handlers'])) {
$drupal_static_fast['handlers'] = &drupal_static(__FUNCTION__, array());
}
$handlers = &$drupal_static_fast['handlers'];
// Workaround the lack of a context object.
if (empty($entity)) {
if (isset($handlers[$entity_type]['#current'])) {
return $handlers[$entity_type]['#current'];
}
elseif (empty($entity_type) && isset($handlers['#current']['#current'])) {
return $handlers['#current']['#current'];
}
else {
return NULL;
}
}
elseif (is_string($entity)) {
$entity = entity_create_stub_entity($entity_type, array(NULL, NULL, $entity));
}
list($entity_id) = entity_extract_ids($entity_type, $entity);
if (!isset($handlers[$entity_type][$entity_id])) {
$entity_info = entity_get_info($entity_type);
$class = $entity_info['translation']['entity_translation']['class'];
// @todo remove fourth parameter once 3rd-party translation handlers have
// been fixed and no longer require the deprecated entity_id parameter.
$handler = new $class($entity_type, $entity_info, $entity, NULL);
// If the entity id is empty we cannot cache the translation handler
// instance.
if (empty($entity_id)) {
return $handler;
}
else {
$handlers[$entity_type][$entity_id] = $handler;
}
}
elseif ($update) {
$handlers[$entity_type][$entity_id]->setEntity($entity);
}
$handlers[$entity_type]['#current'] = $handlers[$entity_type][$entity_id];
$handlers['#current']['#current'] = $handlers[$entity_type][$entity_id];
return $handlers[$entity_type][$entity_id];
function entity_translation_get_handler($entity_type = NULL, $entity = NULL) {
$factory = EntityTranslationHandlerFactory::getInstance();
return empty($entity) ? $factory->getLastHandler($entity_type) : $factory->getHandler($entity_type, $entity);
}
/**
@@ -1811,24 +1905,11 @@ function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $up
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
function entity_translation_entity_form_get_handler($form, &$form_state) {
function entity_translation_entity_form_get_handler($form, $form_state) {
$handler = FALSE;
$entity_type = isset($form['#entity_type']) && is_string($form['#entity_type']) ? $form['#entity_type'] : FALSE;
if ($entity_type) {
if (empty($form_state['storage']['entity_translation']['handler'][$entity_type])) {
if ($info = entity_translation_edit_form_info($form, $form_state)) {
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
$form_state['storage']['entity_translation']['handler'][$info['entity type']] = $handler;
}
}
}
else {
$handler = $form_state['storage']['entity_translation']['handler'][$entity_type];
}
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
}
return $handler;
}
@@ -1837,21 +1918,25 @@ function entity_translation_entity_form_get_handler($form, &$form_state) {
*
* @return EntityTranslationHandlerInterface
* A translation handler instance if available, FALSE oterwise.
*
* @deprecated This is no longer used and will be removed in the first stable
* release.
*/
function entity_translation_current_form_get_handler() {
$handler = FALSE;
if (!empty($_POST['form_build_id'])) {
$form_state = form_state_defaults();
$form = form_get_cache($_POST['form_build_id'], $form_state);
$handler = entity_translation_entity_form_get_handler($form, $form_state);
if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
}
}
return $handler;
}
/**
* Returns an array of edit form info as defined in hook_translation_info().
* Returns an array of edit form info as defined in hook_entity_info().
*
* @param $form
* The entity edit form.
@@ -1869,11 +1954,13 @@ function entity_translation_edit_form_info($form, $form_state) {
if ($entity_type) {
$entity_info = entity_get_info($form['#entity_type']);
if (!empty($entity_info['translation']['entity_translation']['edit form'])) {
$entity_key = $entity_info['translation']['entity_translation']['edit form'];
if (isset($form_state[$entity_key])) {
$entity_keys = explode('][', $entity_info['translation']['entity_translation']['edit form']);
$key_exists = FALSE;
$entity = drupal_array_get_nested_value($form_state, $entity_keys, $key_exists);
if ($key_exists) {
$info = array(
'entity type' => $form['#entity_type'],
'entity' => (object) $form_state[$entity_key],
'entity' => (object) $entity,
);
}
}
@@ -1961,31 +2048,50 @@ function entity_translation_entity_uuid_presave(&$entity, $entity_type) {
}
/**
* Implement hook_pathauto_alias_alter().
* Implements hook_pathauto_alias_alter().
*
* When bulk-updating aliases for nodes automatically create a path for every
* translation.
* When updating or bulk-updating aliases for nodes, automatically create a path
* for every translation, dependent on related configuration.
* @see entity_translation_admin_form()
*/
function entity_translation_pathauto_alias_alter(&$alias, array &$context) {
$info = entity_get_info();
$entity_type = $context['module'];
$operations_modes = array(
'bulkupdate' => variable_get('entity_translation_pathauto_state_mode_bulkupdate', 'generate_all_aliases_all_languages'),
'update' => variable_get('entity_translation_pathauto_state_mode_update', 'generate_alias_active_language'),
);
$operation = $context['op'];
// Ensure that we are dealing with a bundle having entity translation enabled.
if ($context['op'] == 'bulkupdate' && !empty($info[$entity_type]['token type']) && !empty($context['data'][$info[$entity_type]['token type']])) {
$entity = $context['data'][$info[$entity_type]['token type']];
if (entity_translation_enabled($entity_type, $entity)) {
$translations = entity_translation_get_handler($entity_type, $entity)->getTranslations();
// Only create extra aliases if we are working on the original language to
// avoid infinite recursion.
if ($context['language'] == $translations->original) {
foreach ($translations->data as $language => $translation) {
// We already have an alias for the original language, so let's not
// create another one.
if ($language == $translations->original) {
continue;
}
pathauto_create_alias($entity_type, $context['op'], $context['source'], $context['data'], $context['type'], $language);
}
// Nothing to do when we don't have a replacement pattern or incoming data.
if (!in_array($operation, array_keys($operations_modes)) || empty($info[$entity_type]['token type']) || empty($context['data'][$info[$entity_type]['token type']])) {
return;
}
// Do not run additional 'update' or 'bulkupdate' operations if the update
// mode is set to generate an alias only for the active language.
if (in_array($operation, array_keys($operations_modes)) &&
$operations_modes[$operation] === 'generate_alias_active_language') {
return;
}
$entity = $context['data'][$info[$entity_type]['token type']];
// Ensure that we are dealing with an entity bundle having entity translation
// enabled.
if (entity_translation_enabled($entity_type, $entity)) {
$translations = entity_translation_get_handler($entity_type, $entity)->getTranslations();
foreach ($translations->data as $language => $translation) {
// We already have an alias for the starting language, so let's not create
// another one.
if ($language === $context['language']) {
continue;
}
$no_existing_alias = (FALSE === _pathauto_existing_alias_data($context['source'], $language));
if ($operations_modes[$operation] === 'generate_all_aliases_all_languages' ||
($no_existing_alias && $operations_modes[$operation] === 'generate_missing_aliases_all_languages')) {
// Modify the context['op'] by appending '_translation', to avoid infinite
// recursion.
pathauto_create_alias($entity_type, $operation . '_translation', $context['source'], $context['data'], $context['type'], $language);
}
}
}
@@ -1999,3 +2105,31 @@ function path_entity_translation_delete($entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
path_delete(array('source' => $handler->getViewPath(), 'language' => $langcode));
}
/**
* Wrapper for entity_save().
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity object.
*/
function entity_translation_entity_save($entity_type, $entity) {
// Entity module isn't required, but use it if it's available.
if (module_exists('entity')) {
entity_save($entity_type, $entity);
}
// Fall back to field_attach_* functions otherwise.
else {
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
}
}
/**
* Implements hook_field_attach_prepare_translation_alter().
*/
function entity_translation_field_attach_prepare_translation_alter(&$entity, $context) {
$handler = entity_translation_get_handler('node', $entity);
$handler->setActiveLanguage($context['langcode']);
}
@@ -43,14 +43,15 @@ function entity_translation_node_menu_alter(&$items, $backup) {
$callback['file'] = $item['file'];
$callback['module'] = $item['module'];
$access_arguments = array_merge(array(1, $item['access callback']), $item['access arguments']);
$page_arguments = array_merge(array('node', 1, $callback), $item['page arguments']);
}
else {
$callback = FALSE;
$access_arguments = array(1);
$page_arguments = array('node', 1);
}
$items['node/%node/translate']['page callback'] = 'entity_translation_overview';
$items['node/%node/translate']['page arguments'] = array('node', 1, $callback);
$items['node/%node/translate']['page arguments'] = $page_arguments;
$items['node/%node/translate']['access arguments'] = $access_arguments;
$items['node/%node/translate']['access callback'] = 'entity_translation_node_tab_access';
$items['node/%node/translate']['file'] = 'entity_translation.admin.inc';
@@ -249,3 +250,24 @@ function entity_translation_query_comment_filter_alter(QueryAlterableInterface $
}
}
}
/**
* Implements hook_node_update_index().
*
* Add translated node content to search index.
*/
function entity_translation_node_update_index($node) {
$text = '';
$langcodes = array_keys(language_list());
$translations = entity_translation_get_handler('node', $node)->getTranslations();
foreach ($langcodes as $langcode) {
// Skip the default language (already indexed by search.module).
if ($GLOBALS[LANGUAGE_TYPE_CONTENT]->language != $langcode && isset($translations->data[$langcode])) {
// Render the node in each language.
node_build_content($node, 'search_index', $langcode);
$rendered = drupal_render($node->content);
$text .= '<h1>' . check_plain($node->title) . '</h1>' . $rendered;
}
}
return $text;
}
@@ -0,0 +1,568 @@
<?php
/**
* @file
* The taxonomy specific translation functions and hook implementations.
*/
/**
* Returns whether the given taxonomy vocabulary has support for translations.
*
* @return bool
* TRUE if translation is enabled, FALSE otherwise.
*/
function entity_translation_taxonomy_term_enabled_vocabulary($vocabulary_name) {
$info = variable_get('entity_translation_taxonomy', array());
return !empty($info[$vocabulary_name]);
}
/**
* Taxonomy-term-specific menu alterations.
*/
function entity_translation_taxonomy_term_menu_alter(&$items, $backup) {
if (isset($backup['taxonomy_term'])) {
$item = $backup['taxonomy_term'];
// Preserve the menu router item defined by other modules.
$callback['page callback'] = $item['page callback'];
$callback['file'] = $item['file'];
$callback['module'] = $item['module'];
$access_arguments = array_merge(array(2, $item['access callback']), $item['access arguments']);
$page_arguments = array_merge(array('taxonomy_term', 2, $callback), $item['page arguments']);
}
else {
$access_arguments = array(2);
$page_arguments = array('taxonomy_term', 2);
}
$items['taxonomy/term/%taxonomy_term/translate']['page callback'] = 'entity_translation_overview';
$items['taxonomy/term/%taxonomy_term/translate']['page arguments'] = $page_arguments;
$items['taxonomy/term/%taxonomy_term/translate']['access arguments'] = $access_arguments;
$items['taxonomy/term/%taxonomy_term/translate']['access callback'] = 'entity_translation_taxonomy_term_tab_access';
$items['taxonomy/term/%taxonomy_term/translate']['file'] = 'entity_translation.admin.inc';
$items['taxonomy/term/%taxonomy_term/translate']['module'] = 'entity_translation';
// Delete translation callback.
$items['taxonomy/term/%taxonomy_term/translate/delete/%entity_translation_language']['access arguments'] = $access_arguments;
}
/**
* Taxonomy term specific access callback.
*/
function entity_translation_taxonomy_term_tab_access() {
$args = func_get_args();
$term = array_shift($args);
if (entity_translation_enabled('taxonomy_term', $term)) {
return entity_translation_tab_access('taxonomy_term', $term);
}
else {
$function = array_shift($args);
return $function ? call_user_func_array($function, $args) : FALSE;
}
}
/**
* Implements hook_form_FORM_ID_alter()
*/
function entity_translation_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
if (entity_translation_enabled('taxonomy_term')) {
$name = $form_state['vocabulary']->machine_name;
if (isset($form['i18n_translation']['i18n_mode'])) {
$args = array('!url' => url('admin/config/regional/entity_translation'));
$form['i18n_translation']['i18n_mode']['#options'][I18N_MODE_ENTITY_TRANSLATION] = t('Field translation. Term fields will be translated through the <a href="!url">Entity translation</a> module.', $args);
if (entity_translation_enabled_bundle('taxonomy_term', $name)) {
$form['i18n_translation']['i18n_mode']['#default_value'] = I18N_MODE_ENTITY_TRANSLATION;
}
}
else {
$form['entity_translation_taxonomy'] = array(
'#title' => t('Enable field translation'),
'#type' => 'checkbox',
'#prefix' => '<label>' . t('Translation') . '</label>',
'#default_value' => entity_translation_enabled('taxonomy_term', $name),
);
}
$form['#submit'][] = 'entity_translation_form_taxonomy_form_vocabulary_submit';
}
}
/**
* Submit handler for the taxonomy vocabulary form.
*/
function entity_translation_form_taxonomy_form_vocabulary_submit($form, &$form_state) {
if (!empty($form_state['values']['i18n_mode']) && $form_state['values']['i18n_mode'] == I18N_MODE_ENTITY_TRANSLATION) {
$form_state['values']['entity_translation_taxonomy'] = TRUE;
}
$info = variable_get('entity_translation_taxonomy', array());
$info[$form_state['vocabulary']->machine_name] = !empty($form_state['values']['entity_translation_taxonomy']);
variable_set('entity_translation_taxonomy', $info);
}
/**
* Returns a translated label for the specified taxonomy term.
*
* @param object $term
* A taxonomy term object.
* @param string $langcode
* The language the label should be translated in.
*
* @return string
* The translated taxonomy term label.
*
* @internal This is supposed to be used only by the ET taxonomy integration
* code, as it might be removed/replaced in any moment of the ET lifecycle.
*/
function _entity_translation_taxonomy_label($term, $langcode) {
$entity_type = 'taxonomy_term';
if (function_exists('title_entity_label')) {
$label = title_entity_label($term, $entity_type, $langcode);
}
else {
$label = entity_label($entity_type, $term);
}
return (string) $label;
}
/**
* Implements entity_translation_form_field_ui_field_edit_WIDGET_TYPE_form_alter().
*
* {@inheritdoc}
*/
function entity_translation_form_field_ui_field_edit_taxonomy_autocomplete_form_alter(&$form, &$form_state) {
$key = 'entity_translation_taxonomy_autocomplete_translate';
$instance = $form['#instance'];
$field_name = $instance['field_name'];
$entity_type = $instance['entity_type'];
$field = field_info_field($field_name);
$translatable = field_is_translatable($entity_type, $field);
$bundle = !empty($field['settings']['allowed_values'][0]['vocabulary']) ? $field['settings']['allowed_values'][0]['vocabulary'] : NULL;
$access = variable_get('entity_translation_taxonomy_autocomplete', FALSE);
// Add a checkbox to toggle in-place translation for autocomplete widgets.
$form['instance']['settings'][$key] = array(
'#type' => 'checkbox',
'#title' => t('Enable in-place translation of terms'),
'#description' => t('Check this option if you wish to use translation forms to perform in-place translation for terms entered in the original language.'),
'#default_value' => !$translatable && !empty($form['#instance']['settings'][$key]),
'#access' => $access && (!$form_state['field_has_data'] || !$translatable) && entity_translation_enabled('taxonomy_term', $bundle),
'#states' => array(
'visible' => array(':input[name="field[translatable]"]' => array('checked' => FALSE)),
),
'#weight' => -8,
);
}
/**
* Checks whether in-place translation is enabled for the autocomplete widget.
*
* @param array $element
* The widget form element.
* @param array $form_state
* The form state array.
*
* @return bool
* TRUE if in-place translation is enabled, FALSE otherwise.
*/
function _entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state) {
$field = field_info_field($element['#field_name']);
if (field_is_translatable($element['#entity_type'], $field)) {
return FALSE;
}
list($id, , $bundle) = entity_extract_ids($element['#entity_type'], $element['#entity']);
if (!$id) {
return FALSE;
}
$entity_type = 'taxonomy_term';
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$active_langcode = $parent_handler->getActiveLanguage();
$translations = $parent_handler->getTranslations();
$entity_langcode = isset($translations->original) ? $translations->original : LANGUAGE_NONE;
$instance = field_info_instance($element['#entity_type'], $field['field_name'], $bundle);
// We need to make sure that we are not dealing with a translation form.
// However checking the active language is not enough, because the user may
// have changed the entity language.
return
(isset($form_state['entity_translation']['is_translation']) ?
$form_state['entity_translation']['is_translation'] : ($active_langcode != $entity_langcode)) &&
!empty($instance['settings']['entity_translation_taxonomy_autocomplete_translate']) &&
(user_access('translate any entity') || user_access("translate $entity_type entities"));
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter().
*
* {@inheritdoc}
*/
function entity_translation_field_widget_taxonomy_autocomplete_form_alter(&$element, &$form_state, $context) {
// The autocomplete widget is also displayed in the field configuration form,
// in which case we do not need to perform any alteration. To preserve BC, by
// default we enable our taxonomy autocomplete override only on new sites.
if (!isset($element['#entity']) || !variable_get('entity_translation_taxonomy_autocomplete', FALSE)) {
return;
}
// We will need to translate term names, if Title is enabled and configured
// for this vocabulary.
$entity_type = 'taxonomy_term';
$field = field_widget_field($element, $form_state);
$bundle = !empty($field['settings']['allowed_values'][0]['vocabulary']) ? $field['settings']['allowed_values'][0]['vocabulary'] : NULL;
if ($bundle && entity_translation_enabled($entity_type, $bundle)) {
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$langcode = $parent_handler->getActiveLanguage();
$terms = array_values(_entity_translation_taxonomy_autocomplete_widget_get_terms($element));
// If we are using the regular autocomplete behavior also in translation
// forms, we need to set our custom callback.
if (!_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) {
$element['#autocomplete_path'] = 'entity_translation/' . $entity_type . '/autocomplete/' . $langcode . '/' . $element['#field_name'];
$translations = $parent_handler->getTranslations();
if (isset($translations->original) && $translations->original != $langcode) {
$labels = array();
foreach ($terms as $delta => $term) {
$labels[] = _entity_translation_taxonomy_label($term, $langcode);
}
$element['#default_value'] = implode(', ', $labels);
}
}
// Otherwise we just provide the in-place translation widget.
else {
$element['#type'] = 'fieldset';
$element['#description'] = t('Enter one translation for each term');
$element['#access'] = (bool) $terms;
foreach ($terms as $delta => $term) {
$element[$delta] = array(
'#type' => 'textfield',
'#default_value' => _entity_translation_taxonomy_label($term, $langcode),
'#required' => TRUE,
'#tid' => $term->tid,
);
}
$element['#process'][] = 'entity_translation_taxonomy_autocomplete_process';
}
// The native term save logic is performed at widget validation level, so we
// just replace the validation handler to provide our logic instead.
$element['#element_validate'] = array_values(array_diff($element['#element_validate'], array('taxonomy_autocomplete_validate')));
$element['#element_validate'][] = 'entity_translation_taxonomy_autocomplete_validate';
}
}
/**
* Returns the terms referenced by the taxonomy autocomplete widget field.
*
* @param array $element
* The taxonomy autocomplete form element.
*
* @return object[]
* An associative array of taxonomy term object keyed by their identifiers.
*/
function _entity_translation_taxonomy_autocomplete_widget_get_terms($element) {
$items = isset($element['#entity']->{$element['#field_name']}[$element['#language']]) ?
$element['#entity']->{$element['#field_name']}[$element['#language']] : array();
$tids = array_map(function ($item) { return $item['tid']; }, $items);
return taxonomy_term_load_multiple($tids);
}
/**
* Process callback for the ET taxonomy autocomplete widget.
*
* {@inheritdoc}
*/
function entity_translation_taxonomy_autocomplete_process($element) {
// The in-place translation widget makes sense only for untranslatable field,
// which may have the "(all languages)" label suffix. In this case it would be
// confusing so we need to revert that.
$instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
$element['#title'] = check_plain($instance['label']);
return $element;
}
/**
* Entity translation taxonomy autocomplete callback.
*
* @param string $langcode
* The input language.
* @param string $field_name
* The name of the term reference field.
* @param string $tags_typed
* (optional) A comma-separated list of term names entered in the
* autocomplete form element. Only the last term is used for autocompletion.
* Defaults to an empty string.
*
* @see taxonomy_autocomplete()
*/
function entity_translation_taxonomy_term_autocomplete($langcode = NULL, $field_name = '', $tags_typed = '') {
// If the request has a '/' in the search text, then the menu system will have
// split it into multiple arguments, recover the intended $tags_typed.
$args = func_get_args();
// Shift off the $langcode and $field_name arguments.
array_shift($args);
array_shift($args);
$tags_typed = implode('/', $args);
// Make sure the field exists and is a taxonomy field.
if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') {
// Error string. The JavaScript handler will realize this is not JSON and
// will display it as debugging information.
print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name));
exit;
}
// The user enters a comma-separated list of tags. We only autocomplete the
// last tag.
$tags_typed = drupal_explode_tags($tags_typed);
$tag_last = drupal_strtolower(array_pop($tags_typed));
$term_matches = array();
if ($tag_last != '') {
if (!isset($langcode) || $langcode == LANGUAGE_NONE) {
$langcode = $GLOBALS['language_content']->language;
}
// Part of the criteria for the query come from the field's own settings.
$vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field);
$entity_type = 'taxonomy_term';
$query = new EntityFieldQuery();
$query->addTag('taxonomy_term_access');
$query->entityCondition('entity_type', $entity_type);
// If the Title module is enabled and the taxonomy term name is replaced for
// the current bundle, we can look for translated names, otherwise we fall
// back to the regular name property.
if (module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) {
$name_field = 'name_field';
$language_group = 0;
// Do not select already entered terms.
$column = 'value';
if (!empty($tags_typed)) {
$query->fieldCondition($name_field, $column, $tags_typed, 'NOT IN', NULL, $language_group);
}
$query->fieldCondition($name_field, $column, $tag_last, 'CONTAINS', NULL, $language_group);
$query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group);
}
else {
$name_field = 'name';
// Do not select already entered terms.
if (!empty($tags_typed)) {
$query->propertyCondition($name_field, $tags_typed, 'NOT IN');
}
$query->propertyCondition($name_field, $tag_last, 'CONTAINS');
}
// Select rows that match by term name.
$query->propertyCondition('vid', $vocabulary->vid);
$query->range(0, 10);
$result = $query->execute();
// Populate the results array.
$prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
$terms = !empty($result[$entity_type]) ? taxonomy_term_load_multiple(array_keys($result[$entity_type])) : array();
foreach ($terms as $tid => $term) {
$name = _entity_translation_taxonomy_label($term, $langcode);
$n = $name;
// Term names containing commas or quotes must be wrapped in quotes.
if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
$n = '"' . str_replace('"', '""', $name) . '"';
}
$term_matches[$prefix . $n] = check_plain($name);
}
}
drupal_json_output($term_matches);
}
/**
* Returns the vocabulary enabled for the specified taxonomy reference field.
*
* @param array $field
* A field definition.
*
* @return object|null
* A vocabulary object or NULL if none is found.
*/
function _entity_translation_taxonomy_reference_get_vocabulary($field) {
$vocabulary = NULL;
if (!empty($field['settings']['allowed_values'])) {
$vids = array();
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($field['settings']['allowed_values'] as $tree) {
$vids[] = $vocabularies[$tree['vocabulary']]->vid;
}
$vocabulary = taxonomy_vocabulary_load(reset($vids));
}
return $vocabulary;
}
/**
* Form element validate handler for taxonomy term autocomplete element.
*
* {@inheritdoc}
*
* @see taxonomy_autocomplete_validate()
*/
function entity_translation_taxonomy_autocomplete_validate($element, &$form_state) {
$value = array();
list($id) = entity_extract_ids($element['#entity_type'], $element['#entity']);
$is_new = !isset($id);
// This is the language of the parent entity, that we will be applying to new
// terms.
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$langcode = !empty($form_state['entity_translation']['form_langcode']) ?
$form_state['entity_translation']['form_langcode'] : $parent_handler->getActiveLanguage();
// Handle in-place translation.
if (_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) {
// The referenced terms cannot change, so we just need to collect their term
// identifiers. We also build a map of the corresponding deltas for later
// use.
$deltas = array();
foreach (element_children($element) as $delta) {
$tid = $element[$delta]['#tid'];
$deltas[$tid] = $delta;
$value[$delta]['tid'] = $tid;
}
// Save term translations.
$entity_type = 'taxonomy_term';
$name_field = 'name_field';
$source_langcode = $parent_handler->getSourceLanguage();
// This is a validation handler, so we must defer the actual save to the
// submit phase.
$terms_to_save = &$form_state['entity_translation']['taxonomy_autocomplete'][$element['#entity_type']][$id][$element['#field_name']];
foreach (taxonomy_term_load_multiple(array_keys($deltas)) as $term) {
// This is also the right context to perform validation.
$term_translation = $element[$deltas[$term->tid]]['#value'];
if (!$term_translation) {
$instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
drupal_set_message(t('The translations for %field_label cannot be empty.', array('%field_label' => $instance['label'])), 'error', FALSE);
continue;
}
$handler = entity_translation_get_handler($entity_type, $term);
$translations = $handler->getTranslations();
$term_langcode = $handler->getLanguage();
$typed_langcode = $term_langcode != LANGUAGE_NONE ? $langcode : $term_langcode;
// Create a new translation in the active language, if it is missing.
if (!isset($translations->data[$typed_langcode]) && $typed_langcode != LANGUAGE_NONE) {
$translation = array(
'language' => $typed_langcode,
'source' => $source_langcode,
'uid' => $GLOBALS['user']->uid,
'status' => 1,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
$translation_values = array(
$name_field => array($typed_langcode => array(array('value' => $term_translation))),
);
$handler->setTranslation($translation, $translation_values);
$terms_to_save[] = $term;
}
// Otherwise we just update the existing translation, if it has changed.
// If the term is language-neutral, we just update its main value. This is
// expected to happen normally, but could when referencing existing terms.
elseif ($term_translation != _entity_translation_taxonomy_label($term, $typed_langcode)) {
$term->{$name_field}[$typed_langcode][0]['value'] = $term_translation;
$terms_to_save[] = $term;
}
}
}
// Autocomplete widgets do not send their tids in the form, so we must detect
// them here and process them independently.
elseif ($tags = $element['#value']) {
$entity_type = 'taxonomy_term';
$field = field_widget_field($element, $form_state);
$vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field);
$typed_tags = drupal_explode_tags($tags);
// Collect existing terms by name.
$existing_terms = array();
foreach (_entity_translation_taxonomy_autocomplete_widget_get_terms($element) as $term) {
$name = _entity_translation_taxonomy_label($term, $langcode);
$existing_terms[$name] = $term;
}
// Select terms that match by the (translated) name.
$query = new EntityFieldQuery();
$query->addTag('taxonomy_term_access');
$query->entityCondition('entity_type', $entity_type);
$query->propertyCondition('vid', $vocabulary->vid);
if ($langcode != LANGUAGE_NONE && module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) {
$language_group = 0;
// Do not select already entered terms.
$name_field = 'name_field';
$column = 'value';
$query->fieldCondition($name_field, $column, $typed_tags, NULL, NULL, $language_group);
// When we are creating a new entity, we cannot filter by active language,
// as that may have not be applied to the autocomplete query.
if (!$is_new) {
$query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group);
}
}
else {
$query->propertyCondition('name', $typed_tags);
}
$result = $query->execute();
// When we are creating a new entity, the language used for the autocomplete
// query is the current content language, so we should use that to update
// the map of existing terms.
if (!empty($result[$entity_type])) {
$typed_langcode = !$is_new ? $langcode : $GLOBALS['language_content']->language;
foreach (taxonomy_term_load_multiple(array_keys($result[$entity_type])) as $term) {
$name = _entity_translation_taxonomy_label($term, $typed_langcode);
$existing_terms[$name] = $term;
}
}
// Now collect the identifiers for the various terms and update the taxonomy
// reference field values.
foreach ($typed_tags as $delta => $typed_tag) {
// See if the term exists in the chosen vocabulary and return the tid.
// Otherwise create a new 'autocreate' term for insert/update.
if (isset($existing_terms[$typed_tag])) {
$term = $existing_terms[$typed_tag];
}
else {
$term = (object) array(
'tid' => 'autocreate',
'vid' => $vocabulary->vid,
'name' => $typed_tag,
'vocabulary_machine_name' => $vocabulary->machine_name,
);
$handler = entity_translation_get_handler($entity_type, $term);
$handler->setOriginalLanguage($langcode);
$handler->initTranslations();
}
$value[] = (array) $term;
}
}
form_set_value($element, $value, $form_state);
}
/**
* Term-specific implementation of hook_field_attach_submit().
*/
function entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, &$form_state) {
// Finally save in-place translations
if (!empty($form_state['entity_translation']['taxonomy_autocomplete'])) {
foreach ($form_state['entity_translation']['taxonomy_autocomplete'] as $entity_type => $entity_type_data) {
foreach ($entity_type_data as $id => $field_name_data) {
foreach ($field_name_data as $field_name => $term_data) {
if (!is_array($term_data)) {
continue;
}
foreach ($term_data as $term) {
entity_translation_entity_save('taxonomy_term', $term);
}
}
}
}
}
}
@@ -7,9 +7,8 @@ dependencies[] = i18n
dependencies[] = i18n_menu
files[] = entity_translation_i18n_menu.test
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -58,11 +58,10 @@ function entity_translation_i18n_menu_node_presave($node) {
return;
}
// If we change from menu localization to translation sets while working on
// a translation (i.e. not the source node), then leave the source menu item
// intact and create a new one.
// When creating a new translation, leave the source menu item intact and
// create a new one.
$langcode = entity_language('node', $node);
if (!empty($node->menu) && $tset && $node->menu['language'] == LANGUAGE_NONE && !empty($source_langcode)) {
if (!empty($node->menu) && $tset && !empty($source_langcode)) {
$node->source_menu = menu_link_load($node->menu['mlid']);
unset($node->menu['mlid']);
}
@@ -165,7 +164,7 @@ function entity_translation_i18n_menu_form(&$form, &$form_state) {
$default = isset($source_menu['language']) && $source_menu['language'] != LANGUAGE_NONE;
$languages = language_list();
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$langcode = $handler->getFormLanguage();
$langcode = $handler->getActiveLanguage();
$language_name = isset($languages[$langcode]) ? t($languages[$langcode]->name) : t('current');
$form['menu']['#multilingual'] = TRUE;
@@ -197,8 +196,8 @@ function entity_translation_i18n_menu_form_menu_edit_item_validate($form, &$form
$item = $form_state['values'];
// Localizable menu items should not be created when a translation set for the
// same path already exists.
if ($item['language'] == LANGUAGE_NONE) {
// same path already exists (exluding special paths starting by <).
if ($item['language'] == LANGUAGE_NONE && strpos($item['link_path'], '<') !== 0) {
$count = db_select('menu_links', 'ml')
->condition('ml.link_path', $item['link_path'])
->condition('ml.i18n_tsid', 0, '<>')
@@ -262,7 +261,7 @@ function entity_translation_i18n_menu_item($item) {
* @param $node
* A node object, with a menu item ($node->menu).
* @param $langcode
* The language into which the menu item should be translates.
* The language into which the menu item should be translated.
*/
function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode) {
// Localization.
@@ -272,7 +271,7 @@ function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode)
// used for the node menu form; i18n_menu_link_localize only localizes
// rendered properties 'title' and 'localized_options.attributes.title'.
$node->menu['link_title'] = $node->menu['title'];
$node->menu['options']['attributes']['title'] = $node->menu['localized_options']['attributes']['title'];
$node->menu['options']['attributes']['title'] = isset($node->menu['localized_options']['attributes']['title']) ? $node->menu['localized_options']['attributes']['title'] : '';
}
// Translation sets.
else {
@@ -288,7 +287,9 @@ function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode)
// Make sure the menu item is not set to hidden; i18n_menu automatically
// hides any menu items not matching the current interface language.
$menu['hidden'] = FALSE;
if (!empty($menu)) {
$menu['hidden'] = FALSE;
}
}
// Replace the menu item with the translated version, or null if there is
@@ -368,6 +369,11 @@ function entity_translation_i18n_menu_entity_translation_upgrade($node, $transla
* Implements hook_entity_translation_delete().
*/
function entity_translation_i18n_menu_entity_translation_delete($entity_type, $entity, $langcode) {
// Make sure that we are working with an entity of type node.
if ($entity_type != 'node') {
return;
}
list($entity_id, , ) = entity_extract_ids($entity_type, $entity);
// Clean-up all menu module links that point to this node.
@@ -26,6 +26,7 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->login($this->getAdminUser(array('administer menu')));
$this->addLanguage('en');
$this->addLanguage('es');
$this->addLanguage('it');
$this->configureContentType();
$this->configureMenu();
$this->enableUrlLanguageDetection();
@@ -45,13 +46,13 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
/**
* Create page with menu item.
*/
function createPage($link_title, $langcode) {
function createPage($link_title, $description, $langcode) {
$edit = array();
$edit['title'] = $this->randomName();
$edit['language'] = $langcode;
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = 'link language = ' . $langcode;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $edit['title'])), t('Basic page created.'));
@@ -69,14 +70,16 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
/**
* Create a translation with menu item.
*/
function createTranslation($node, $link_title, $langcode, $use_tsets = FALSE) {
function createTranslation($node, $link_title, $description, $langcode, $use_tsets = FALSE) {
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $node->language . '/' . $langcode);
$edit = array();
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = 'link language = ' . $langcode;
$edit['menu[tset]'] = $use_tsets;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
if ($use_tsets != NULL) {
$edit['menu[tset]'] = $use_tsets;
}
$this->drupalPost(NULL, $edit, t('Save'));
$this->drupalGet('node/' . $node->nid . '/translate');
@@ -122,23 +125,36 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->assertText($link_title, 'New menu link title found: ' . $link_title);
}
/**
* Asserts a link with a given label and title is found in the page.
*/
function assertLinkWithTitleAttribute($label, $title) {
$links = $this->xpath('//a[normalize-space(text())=:label and normalize-space(@title)=:title]', array(
':label' => $label,
':title' => $title,
));
$this->assert(isset($links[0]));
}
/**
* Test if menu localization works.
*/
function testMenuLocalization() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, 'en');
$link_description_en = $this->randomName() . ' & htmlentity';
$node = $this->createPage($link_title_en, $link_description_en, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, 'es');
$link_description_es = $this->randomName() . ' & htmlentity';
$node_translation = $this->createTranslation($node, $link_title_es, $link_description_es, 'es');
// Check menu links in both languages.
$this->get('en', '<front>');
$this->assertText($link_title_en);
$this->assertLinkWithTitleAttribute($link_title_en, $link_description_en);
$this->get('es', '<front>');
$this->assertText($link_title_es);
$this->assertLinkWithTitleAttribute($link_title_es, $link_description_es);
// Edit English menu link.
$link_title_en2 = $this->randomName();
@@ -146,15 +162,19 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
// Check that Spanish menu link has not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es);
$this->assertLinkWithTitleAttribute($link_title_es, $link_description_es);
// Edit Spanish menu link.
$link_title_es2 = $this->randomName();
$this->editPage($node, $link_title_es, $link_title_es2, 'es');
// Check that English menu link has not changed.
// Check that English menu link has changed.
$this->get('en', '<front>');
$this->assertText($link_title_en2);
$this->assertLinkWithTitleAttribute($link_title_en2, $link_description_en);
// Check that Spanish menu link has changed.
$this->get('es', '<front>');
$this->assertLinkWithTitleAttribute($link_title_es2, $link_description_es);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
@@ -169,11 +189,11 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
function testMenuLocalizationCustomSourceLanguage() {
// Create Basic page in Spanish.
$link_title_es = $this->randomName();
$node = $this->createPage($link_title_es, 'es');
$node = $this->createPage($link_title_es, NULL, 'es');
// Submit translation in English.
$link_title_en = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_en, 'en');
$node_translation = $this->createTranslation($node, $link_title_en, NULL, 'en');
// Check menu links in both languages.
$this->get('es', '<front>');
@@ -204,11 +224,11 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
function testMenuTranslation() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, 'en');
$node = $this->createPage($link_title_en, NULL, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, 'es', TRUE);
$node_translation = $this->createTranslation($node, $link_title_es, NULL, 'es', TRUE);
// Check menu links in both languages.
$this->get('en', '<front>');
@@ -232,6 +252,17 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Add another translation, and check that other menu items are not
// affected. See https://drupal.org/node/1982140
$link_title_it = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_it, NULL, 'it', NULL);
// Check that Spanish and English menu links have not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es2);
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
$this->removeTranslation($node, 'es');
@@ -34,8 +34,8 @@ function entity_translation_upgrade_start($types) {
*/
function entity_translation_upgrade_end($success, $results, $operations, $elapsed) {
if (!empty($results)) {
$message = format_plural($results, '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results), WATCHDOG_INFO);
$message = format_plural($results['total'], '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results['total']), WATCHDOG_INFO);
}
else {
$message = t('No node translation available for the upgrade.');
@@ -91,6 +91,10 @@ function entity_translation_upgrade_do($types, &$context) {
$node = $nodes[$nid];
$original = $nodes[$node->tnid];
$handler = entity_translation_get_handler('node', $original);
// Instantiate the data and original properties of the translations.
if (empty($handler->getTranslations()->data)) {
$handler->initTranslations();
}
if (!isset($instances[$node->type])) {
$instances[$node->type] = field_info_instances('node', $node->type);
@@ -185,7 +189,7 @@ function entity_translation_upgrade_do($types, &$context) {
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
if ($context['finished'] >= 1) {
$context['results'] = $context['sandbox']['total'];
$context['results']['total'] = $context['sandbox']['total'];
}
}
}
@@ -0,0 +1,117 @@
<?php
/**
* Implements hook_drush_command().
*/
function entity_translation_upgrade_drush_command() {
$items = array();
$items['entity-translation-upgrade'] = array(
'description' => "Upgrades all nodes of the specified content type from Content Translation to Entity Translation.",
'arguments' => array(
'content_type' => 'Content type of nodes to be upgraded.',
),
'examples' => array(
'drush entity-translation-upgrade article' => 'Upgrades all nodes of content type "article" from Content Translation to Entity Translation.',
),
'aliases' => array('etu'),
'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
);
return $items;
}
/**
* Implements hook_drush_help().
*
* @param
* A string with the help section
*
* @return
* A string with the help text for the entity-translation-upgrade command
*/
function entity_translation_upgrade_drush_help($section) {
switch ($section) {
case 'drush:entity-translation-upgrade':
return dt("Brief help for Drush command entity-translation-upgrade.");
case 'meta:entity_translation_upgrade:title':
return dt("Entity Translation Upgrade commands");
case 'meta:entity_translation_upgrade:summary':
return dt("Upgrading nodes to Entity Translation.");
}
}
/**
* Implements drush_hook_COMMAND().
*
* @param
* The content type of which the nodes shall be upgraded
*
* Runs the batch upgrading nodes of the specified content_type to Entity
* Translation. Lets user choose content type from a list, if argument has
* not been provided.
*/
function drush_entity_translation_upgrade($content_type = "") {
// Get all installed content types.
$available_types = array();
$available_types_chose = array();
$available_types_str = '';
foreach (node_type_get_types() as $type) {
$available_types[$type->type] = $type->type;
$available_types_chose[$type->type] = $type->name;
if (strlen($available_types_str) > 0) {
$available_types_str .= ', ';
}
$available_types_str .= $type->type;
}
// If argument content_type is empty, prompt user for content type.
if (!$content_type) {
$content_type = drush_choice($available_types_chose, dt('Choose the content type of the nodes to be upgraded to Entity Translation:'));
}
// Do content type argument checks.
if (!$content_type) {
return TRUE;
}
if (strlen($available_types_str) == 0) {
return drush_set_error(dt('Entity Translation Upgrade cannot run as no content type has been installed.'));
}
if (!in_array($content_type, $available_types)) {
return drush_set_error(dt('"@content_type" is not a valid content type machine name. Please use one of these installed content types as argument: @available_types_str.', array('@content_type' => $content_type, '@available_types_str' => $available_types_str)));
}
// Start batch to upgrade nodes of the specified content type.
$types = array($content_type => $content_type);
$batch = array(
'operations' => array(
array('entity_translation_upgrade_do', array($types)),
array('entity_translation_upgrade_complete', array()),
),
'finished' => 'entity_translation_upgrade_drush_end',
'file' => drupal_get_path('module', 'entity_translation_upgrade') . '/entity_translation_upgrade.admin.inc',
'progressive' => FALSE,
);
batch_set($batch);
drush_backend_batch_process();
}
/**
* This is the 'finished' batch callback, drush version.
*/
function entity_translation_upgrade_drush_end($success, $results, $operations, $elapsed) {
// Print result messages.
if (!empty($results)) {
$message = format_plural($results['total'], '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
$severity = 'ok';
watchdog('Entity Translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results['total']), WATCHDOG_INFO);
}
else {
$message = t('No node translation available for the upgrade.');
$severity = 'warning';
}
drush_log($message, $severity);
}
@@ -3,10 +3,10 @@ description = Provides an upgrade path from node-based translation to field-base
package = Multilingual - Entity Translation
core = 7.x
dependencies[] = entity_translation
files[] = entity_translation_upgrade.test
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -0,0 +1,163 @@
<?php
/**
* @file
* Tests for Entity Translation module.
*/
/**
* Tests for the upgrade translation process.
*/
class EntityTranslationUpgradeTestCase extends EntityTranslationTestCase {
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Translation upgrade',
'description' => 'Tests for the upgrade from Content Translation to Entity Translation.',
'group' => 'Entity translation',
'dependencies' => array(),
);
}
function setUp() {
parent::setUp('locale', 'translation', 'translation_test', 'entity_translation', 'entity_translation_upgrade');
$this->getAdminUser(array(
'toggle field translatability',
'administer entity translation',
));
$this->getTranslatorUser(array(
'translate content',
));
$this->login($this->getAdminUser());
$this->addLanguage('en');
$this->addLanguage('es');
$this->configureContentTypeForContentTranslation();
$this->enableUrlLanguageDetection();
$this->login($this->getTranslatorUser());
}
/**
* Configure the "Basic page" content type for entity translation tests.
*/
public function configureContentTypeForContentTranslation() {
// Configure the "Basic page" content type to use multilingual support with
// content translation.
$edit = array();
$edit['language_content_type'] = TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.'));
}
/**
* Toggle body field's translatability.
*/
public function makeBodyFieldTranslatable() {
$edit = array();
$this->drupalGet('admin/structure/types/manage/page/fields/body');
$this->clickLink('Enable translation');
$this->drupalPost(NULL, array(), t('Confirm'));
$this->assertRaw(t('Data successfully processed.'), t('Body field have been made translatable.'));
}
/**
* @see TranslationTestCase::createPage
*/
function createContentTranslationPage($title, $body, $language = NULL) {
$edit = array();
$langcode = LANGUAGE_NONE;
$edit["title"] = $title;
$edit["body[$langcode][0][value]"] = $body;
if (!empty($language)) {
$edit['language'] = $language;
}
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Basic page created.');
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($title);
$this->assertTrue($node, 'Node found in database.');
return $node;
}
/**
* @see TranslationTestCase::createTranslation
*/
function createContentTranslationTranslation($node, $title, $body, $language) {
$this->drupalGet('node/add/page', array('query' => array('translation' => $node->nid, 'target' => $language)));
$langcode = LANGUAGE_NONE;
$body_key = "body[$langcode][0][value]";
$this->assertFieldByXPath('//input[@id="edit-title"]', $node->title, "Original title value correctly populated.");
$this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[LANGUAGE_NONE][0]['value'], "Original body value correctly populated.");
$edit = array();
$edit["title"] = $title;
$edit[$body_key] = $body;
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Translation created.');
// Check to make sure that translation was successful.
$translation = $this->drupalGetNodeByTitle($title);
$this->assertTrue($translation, 'Node found in database.');
$this->assertTrue($translation->tnid == $node->nid, 'Translation set id correctly stored.');
return $translation;
}
/**
* Tests copying of source node's body value in the add translation form page.
*/
public function testUpgradeContentToEntityTranslation() {
// Create Basic page in English.
$node_title = $this->randomName();
$node_body = $this->randomName();
$node = $this->createContentTranslationPage($node_title, $node_body, 'en');
// Submit translation in Spanish.
$this->drupalGet('node/' . $node->nid . '/translate');
$node_translation_title = $this->randomName();
$node_translation_body = $this->randomName();
$node_translation = $this->createContentTranslationTranslation($node, $node_translation_title, $node_translation_body, 'es');
// Make Body field translatable before we run the upgrade.
$this->login($this->getAdminUser());
$this->makeBodyFieldTranslatable();
// Run the upgrade for all Page nodes.
$edit = array(
'types[page]' => 'page',
);
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Upgrade'));
// Switch to our translator user.
$this->login($this->getTranslatorUser());
// Check that the unpublished target node triggers a redirect.
$this->drupalGet('node/' . $node_translation->nid);
$headers = $this->drupalGetHeaders(TRUE);
list(, $status) = explode(' ', $headers[0][':status'], 3);
$this->assertEqual($status, 301, 'Expected response code was sent.');
$languages = language_list();
$this->assertEqual($this->getUrl(), url('node/' . $node->nid, array('absolute' => TRUE, 'language' => $languages['es'])), 'entity_translation_upgrade_redirect() redirected to expected URL.');
// Check that the body is displayed when the active language is English.
$this->drupalGet('node/' . $node->nid);
$this->assertRaw($node_body, t('Body field displayed correctly in the source language.'));
// Check that the translated body is displayed when the active language is Spanish.
$this->drupalGet('es/node/' . $node->nid);
$this->assertRaw($node_translation_body, t('Body field displayed correctly in the target language.'));
// Check that the edit forms are initialized correctly in the target language.
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertFieldByXPath("//textarea[@name='body[en][0][value]']", $node_body, "Body field correctly instantiated with the value of the source language.");
$this->drupalGet('es/node/' . $node->nid . '/edit');
$this->assertFieldByXPath("//textarea[@name='body[es][0][value]']", $node_translation_body, "Body field correctly instantiated with the value of the target language.");
}
}
@@ -14,6 +14,21 @@
*/
interface EntityTranslationHandlerInterface {
/**
* Injects the translation handler factory.
*/
public function setFactory(EntityTranslationHandlerFactory $factory);
/**
* Registers a child translation handler for the given entity.
*/
public function addChild($entity_type, $entity);
/**
* Removes a previously registered child translation handler.
*/
public function removeChild($entity_type, $entity);
/**
* Loads the translation data into the wrapped entity.
*/
@@ -62,6 +77,11 @@ interface EntityTranslationHandlerInterface {
*/
public function removeTranslations();
/**
* Removes all translations from the current revision.
*/
public function removeRevisionTranslations();
/**
* Initialize the language of the original field values.
*
@@ -78,6 +98,62 @@ interface EntityTranslationHandlerInterface {
*/
public function getLanguage();
/**
* Sets the active language.
*
* This is the language that determines which translation should be considered
* "active" for the wrapped entity. The "Entity Translation" module uses this
* information to implement the UI-based translation workflow. Other modules
* can rely on it to implement their own entity translation-related logic.
*
* This will affect which language is returned by the core "entity_language()"
* function.
*
* @param string $langcode
* The active language code.
*
* @see entity_language()
* @see entity_translation_language()
* @see ::getActiveLanguage()
*/
public function setActiveLanguage($langcode);
/**
* Returns the active language.
*
* @return string
* The active language for the wrapped entity.
*
* @see ::setActiveLanguage()
*/
public function getActiveLanguage();
/**
* Sets the active form language.
*
* @param string $langcode
* The active form language code.
*
* @deprecated in 7.x-1.0-beta6, will be removed before 7.x-1.0. Use
* ::setActiveLanguage() instead.
*
* @see ::setActiveLanguage()
*/
public function setFormLanguage($langcode);
/**
* Retrieves the active form language.
*
* @return string
* The active form language code.
*
* @deprecated in 7.x-1.0-beta6, will be removed before 7.x-1.0. Use
* ::getActiveLanguage() instead.
*
* @see ::getActiveLanguage()
*/
public function getFormLanguage();
/**
* Returns the translation object key for the wrapped entity type.
*/
@@ -89,7 +165,7 @@ interface EntityTranslationHandlerInterface {
public function getDefaultLanguage();
/**
* Sets the language of the orginal translation.
* Sets the language of the original translation.
*
* @param $langcode
* The language code of the original content values.
@@ -98,6 +174,9 @@ interface EntityTranslationHandlerInterface {
/**
* Returns TRUE if the entity is currently being translated.
*
* @deprecated This is no longer used and will be removed before the first
* stable release.
*/
public function isTranslating();
@@ -106,6 +185,9 @@ interface EntityTranslationHandlerInterface {
*
* @param $translating
* A boolean value.
*
* @deprecated This is no longer used and will be removed before the first
* stable release.
*/
public function setTranslating($translating);
@@ -114,6 +196,11 @@ interface EntityTranslationHandlerInterface {
*/
public function isRevision();
/**
* Return TRUE if the entity type supports revisions.
*/
public function isRevisionable();
/**
* Replaces the wrapped entity.
*
@@ -122,6 +209,22 @@ interface EntityTranslationHandlerInterface {
*/
public function setEntity($entity);
/**
* Returns the wrapped entity.
*
* @param return
* The wrapped entity.
*/
public function getEntity();
/**
* Returns the wrapped entity type.
*
* @param return
* The wrapped entity type.
*/
public function getEntityType();
/**
* Checks that the wrapped entity matches the give entity
*
@@ -225,16 +328,6 @@ interface EntityTranslationHandlerInterface {
*/
public function isAliasEnabled();
/**
* Sets the active form language.
*/
public function setFormLanguage($langcode);
/**
* Retrieves the active form language.
*/
public function getFormLanguage();
/**
* Sets the source language for the translation being created.
*/
@@ -301,20 +394,35 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected $entityInfo;
protected $entityId;
protected $bundle;
protected $revisionable;
private $entityForm;
private $translating;
private $outdated;
private $formLanguage;
private $sourceLanguage;
/**
* The translation handler factory.
*
* @var EntityTranslationHandlerFactory
*/
protected $factory;
private $pathScheme;
private $pathWildcard;
private $basePath;
private $editPath;
private $translatePath;
private $viewPath;
private $routerMap;
/**
* The translation handler hierarchy storage.
*
* @var array
*/
protected $children = array();
protected $entityForm;
protected $translating;
protected $outdated;
protected $activeLanguage;
protected $sourceLanguage;
protected $pathScheme;
protected $pathWildcard;
protected $basePath;
protected $editPath;
protected $translatePath;
protected $viewPath;
protected $routerMap;
/**
* Initializes an instance of the translation handler.
@@ -334,7 +442,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->entityForm = FALSE;
$this->translating = FALSE;
$this->outdated = FALSE;
$this->formLanguage = FALSE;
$this->activeLanguage = FALSE;
$this->sourceLanguage = FALSE;
$this->pathScheme = 'default';
$this->routerMap = array();
@@ -357,17 +465,29 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return;
}
$revisionable = self::isEntityTypeRevisionable($entity_type);
$revisions_ids = array();
foreach ($entities as $id => $entity) {
$entities[$id]->{$translations_key} = self::emptyTranslations();
if ($revisionable) {
list(, $revisions_id,) = entity_extract_ids($entity_type, $entity);
$revisions_ids[$id] = $revisions_id;
}
}
$results = db_select('entity_translation', 'et')
$table = $revisionable ? 'entity_translation_revision' : 'entity_translation';
$query = db_select($table, 'et')
->fields('et')
->condition('entity_type', $entity_type)
->condition('entity_id', array_keys($entities), 'IN')
->orderBy('entity_id')
->orderBy('created')
->execute();
->condition('entity_type', $entity_type);
if (!$revisionable) {
$query->condition('entity_id', array_keys($entities), 'IN');
}
else {
$query->condition('revision_id', $revisions_ids, 'IN');
}
$results = $query->execute();
foreach ($results as $row) {
$id = $row->entity_id;
@@ -394,6 +514,46 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return $links;
}
/**
* @see EntityTranslationHandlerInterface::setFactory()
*/
public function setFactory(EntityTranslationHandlerFactory $factory) {
$this->factory = $factory;
}
/**
* @see EntityTranslationHandlerInterface::addChild()
*/
public function addChild($entity_type, $entity) {
if (!empty($this->factory)) {
$handler = $this->factory->getHandler($entity_type, $entity);
$handler->setActiveLanguage($this->getActiveLanguage());
$handler->setSourceLanguage($this->getSourceLanguage());
// Avoid registering more than one child handler for each entity.
$hid = $this->factory->getHandlerId($entity_type, $entity);
$this->children[$hid] = $handler;
}
}
/**
* @see EntityTranslationHandlerInterface::removeChild()
*/
public function removeChild($entity_type, $entity) {
if (!empty($this->factory)) {
$hid = $this->factory->getHandlerId($entity_type, $entity);
unset($this->children[$hid]);
}
}
/**
* Proxies the specified method invocation to a child translation handler.
*/
protected function notifyChildren($method, $args) {
foreach ($this->children as $handler) {
call_user_func_array(array($handler, $method), $args);
}
}
/**
* @see EntityTranslationHandlerInterface::loadTranslations()
*/
@@ -410,43 +570,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::saveTranslations()
*/
public function saveTranslations() {
// Delete and insert, rather than update, in case a value was added.
db_delete('entity_translation')
->condition('entity_type', $this->entityType)
->condition('entity_id', $this->entityId)
->execute();
$translations = $this->getTranslations();
if (count($translations->data)) {
global $user;
// Save current values.
$this->doSaveTranslations($translations, 'entity_translation');
$columns = array('entity_type', 'entity_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
$query = db_insert('entity_translation')->fields($columns);
// These values should overridde the translation ones as they are not
// supposed to change.
$overrides = array(
'entity_id' => $this->entityId,
'entity_type' => $this->entityType,
);
// These instead are just defaults.
$defaults = array(
'source' => '',
'uid' => $user->uid,
'translate' => 0,
'status' => 0,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
foreach ($translations->data as $langcode => $translation) {
$translation = $overrides + $translation + $defaults;
$query->values($translation);
}
$query->execute();
// Save revision values.
if ($this->isRevisionable()) {
$this->doSaveTranslations($translations, 'entity_translation_revision', TRUE);
}
// The translation handler interface decouples operations on translations at
@@ -476,6 +607,54 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
/**
* Saves entity translation records to the storage.
*/
protected function doSaveTranslations($translations, $table, $revision = FALSE) {
// Delete and insert, rather than update, in case a value was added.
$query = db_delete($table)
->condition('entity_type', $this->entityType)
->condition('entity_id', $this->entityId);
// If we are storing translations for the current revision or we are
// deleting the entity we should remove all translation data.
$langcode = $translations->original;
$hook = isset($translations->hook) ? $translations->hook : array();
if ($revision && $this->isRevisionable() && (empty($hook[$langcode]['hook']) || $hook[$langcode]['hook'] != 'delete')) {
$query->condition('revision_id', $this->revisionId);
}
$query->execute();
if (count($translations->data)) {
$columns = array('entity_type', 'entity_id', 'revision_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
$query = db_insert($table)->fields($columns);
// These values should override the translation ones as they are not
// supposed to change.
$overrides = array(
'entity_type' => $this->entityType,
'entity_id' => $this->entityId,
'revision_id' => $this->isRevisionable() ? $this->revisionId : $this->entityId,
);
// These instead are just defaults.
$defaults = array(
'source' => '',
'uid' => $GLOBALS['user']->uid,
'translate' => 0,
'status' => 0,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
foreach ($translations->data as $translation) {
$translation = $overrides + $translation + $defaults;
$query->values($translation);
}
$query->execute();
}
}
/**
* @see EntityTranslationHandlerInterface::getTranslations()
*/
@@ -495,6 +674,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setTranslation()
*/
public function setTranslation($translation, $values = NULL) {
$args = func_get_args();
if (isset($translation['source']) && $translation['language'] == $translation['source']) {
throw new Exception('Invalid translation language');
}
@@ -525,11 +705,13 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach (field_info_instances($this->entityType, $this->bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable'] && isset($values[$field_name])) {
if ($field['translatable'] && isset($values[$field_name][$langcode])) {
$this->entity->{$field_name}[$langcode] = $values[$field_name][$langcode];
}
}
}
$this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -600,7 +782,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if (empty($this->getTranslations()->data)) {
$this->initTranslations();
}
elseif (!empty($langcode) && !$this->isTranslating()) {
elseif (!empty($langcode)) {
$this->setOriginalLanguage($langcode);
}
}
@@ -612,6 +794,17 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->removeTranslation(NULL);
}
/**
* @see EntityTranslationHandlerInterface::removeRevisionTranslations()
*/
public function removeRevisionTranslations() {
$translations_key = $this->getTranslationsKey();
$keys = array_keys($this->entity->{$translations_key}->data);
$values = array_fill(0, count($keys), array('hook' => 'delete_revision'));
$this->removeTranslation(NULL);
$this->entity->{$translations_key}->hook = array_combine($keys, $values);
}
/**
* @see EntityTranslationHandlerInterface::initOriginalTranslation()
*/
@@ -656,6 +849,55 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
/**
* {@inheritdoc}
*/
public function setActiveLanguage($langcode) {
// @todo To fully preserve BC, we proxy the call to the deprecated
// ::setFormLanguage method. This will keep things working even when it
// has been overridden. Inline its implementation here upon removal.
$this->setFormLanguage($langcode);
}
/**
* {@inheritdoc}
*/
public function getActiveLanguage() {
// @todo To fully preserve BC, we proxy the call to the deprecated
// ::getFormLanguage method. This will keep things working even when it
// has been overridden. Inline its implementation here upon removal.
return $this->getFormLanguage();
}
/**
* {@inheritdoc}
*/
public function setFormLanguage($langcode) {
$args = func_get_args();
$this->activeLanguage = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
/**
* {@inheritdoc}
*/
public function getFormLanguage() {
if (!empty($this->activeLanguage)) {
return $this->activeLanguage;
}
// For new entities the active language should match the default language.
// The language stored with the entity itself (for example, $node->language)
// may not be reliable since the function creating the entity object will
// not know which language "Entity Translation" is configured to create the
// entity in.
elseif ($this->isNewEntity()) {
return $this->getDefaultLanguage();
}
else {
return $this->getLanguage();
}
}
/**
* @see EntityTranslationHandlerInterface::getLanguageKey()
*/
@@ -700,15 +942,19 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setOriginalLanguage()
*/
public function setOriginalLanguage($langcode) {
$args = func_get_args();
$translations = $this->getTranslations();
if (isset($translations->original) && $translations->original != $langcode) {
$translations->data[$langcode] = $translations->data[$translations->original];
$translations->data[$langcode]['language'] = $langcode;
unset($translations->data[$translations->original]);
}
if (!isset($translations->original) || $translations->original != $langcode) {
if (isset($translations->original)) {
$translations->data[$langcode] = $translations->data[$translations->original];
$translations->data[$langcode]['language'] = $langcode;
unset($translations->data[$translations->original]);
}
$translations->original = $langcode;
$translations->original = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
}
/**
@@ -732,6 +978,25 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return FALSE;
}
/**
* @see EntityTranslationHandlerInterface::isRevisionable()
*/
public function isRevisionable() {
$result = FALSE;
if (!isset($this->revisionable)) {
$result = self::isEntityTypeRevisionable($this->entityType);
}
return $result;
}
/**
* Returns whether the entity type is revisionable.
*/
public static function isEntityTypeRevisionable($entity_type) {
$entity_info = entity_get_info($entity_type);
return variable_get('entity_translation_revision_enabled', FALSE) && !empty($entity_info['entity keys']['revision']);
}
/**
* @see EntityTranslationHandlerInterface::setEntity()
*/
@@ -744,8 +1009,27 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->entity->{$translations_key} = self::emptyTranslations();
}
// Update bundle and entity id properties.
list($this->entityId, , $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
// Update entity properties.
list($this->entityId, $this->revisionId, $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
// Initialize the handler id if needed.
if (!empty($this->factory)) {
$this->factory->getHandlerId($this->entityType, $entity);
}
}
/**
* @see EntityTranslationHandlerInterface::getEntity()
*/
public function getEntity() {
return $this->entity;
}
/**
* @see EntityTranslationHandlerInterface::getEntityType()
*/
public function getEntityType() {
return $this->entityType;
}
/**
@@ -760,13 +1044,15 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setOutdated()
*/
public function setOutdated($outdated) {
$args = func_get_args();
if ($outdated) {
$translations = $this->getTranslations();
foreach ($translations->data as $langcode => &$translation) {
if ($langcode != $this->getFormLanguage()) {
if ($langcode != $this->getActiveLanguage()) {
$translation['translate'] = 1;
}
}
$this->notifyChildren(__FUNCTION__, $args);
}
}
@@ -906,7 +1192,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*/
public function getSharedFieldsAccess() {
$settings = entity_translation_settings($this->entityType, $this->bundle);
return ($settings['shared_fields_original_only'] == FALSE || $this->getLanguage() == $this->getFormLanguage()) &&
return ($settings['shared_fields_original_only'] == FALSE || $this->getLanguage() == $this->getActiveLanguage()) &&
(!entity_translation_workflow_enabled() || user_access('edit translation shared fields') || user_access("edit {$this->entityType} translation shared fields"));
}
@@ -917,25 +1203,13 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return !empty($this->entityInfo['translation']['entity_translation']['alias']);
}
/**
* @see EntityTranslationHandlerInterface::setFormLanguage()
*/
public function setFormLanguage($langcode) {
$this->formLanguage = $langcode;
}
/**
* @see EntityTranslationHandlerInterface::getFormLanguage()
*/
public function getFormLanguage() {
return !empty($this->formLanguage) ? $this->formLanguage : $this->getLanguage();
}
/**
* @see EntityTranslationHandlerInterface::setSourceLanguage()
*/
public function setSourceLanguage($langcode) {
$args = func_get_args();
$this->sourceLanguage = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -966,7 +1240,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function entityForm(&$form, &$form_state) {
$this->entityForm = TRUE;
$translations = $this->getTranslations();
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$langcode = $this->getLanguage();
$is_translation = $this->isTranslationForm();
$new_translation = !isset($translations->data[$form_langcode]);
@@ -974,12 +1248,16 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$languages = language_list();
$access = user_access('translate any entity') || user_access("translate $this->entityType entities");
// Store contextual information in the form state.
$form_state['entity_translation']['form_langcode'] = $form_langcode;
$form_state['entity_translation']['source_langcode'] = $this->getSourceLanguage();
// The only way to determine whether we are editing the original values is
// comparing form language and entity language. Since a language change
// might render impossible to make this check after form submission, we
// store the related information here.
$form_state['entity_translation']['is_translation'] = $is_translation;
// Adjust page title to specify the current language being edited, if we
// have at least one translation.
if ($form_langcode != LANGUAGE_NONE && (!$no_translations || $new_translation)) {
@@ -1019,7 +1297,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
// Add the entity language switcher.
$this->entityFormLanguageWidget($form, $form_state);
if ($is_translation) {
if ($is_translation && isset($form['actions']['delete'])) {
// Replace the delete button with the delete translation one.
if (!$new_translation) {
$weight = 100;
@@ -1082,7 +1360,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
'#disabled' => !$enabled,
);
$translate = !$new_translation && $translations->data[$form_langcode]['translate'];
$translate = !$new_translation && !empty($translations->data[$form_langcode]['translate']);
if (!$translate) {
$form['translation']['retranslate'] = array(
'#type' => 'checkbox',
@@ -1101,7 +1379,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
);
}
$name = $new_translation ? $GLOBALS['user']->name : user_load($translations->data[$form_langcode]['uid'])->name;
$translation_author = $new_translation ? $GLOBALS['user'] : user_load($translations->data[$form_langcode]['uid']);
$name = isset($translation_author->name) ? $translation_author->name : '';
$form['translation']['name'] = array(
'#type' => 'textfield',
'#title' => t('Authored by'),
@@ -1142,8 +1421,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* Either remove access or add a translatability clue depending on the current
* user's "edit translation shared fields" permissions.
*/
public function entityFormSharedElements(&$element) {
static $ignored_types, $shared_labels, $access;
public function entityFormSharedElements(&$element, $access = NULL) {
static $ignored_types, $shared_labels;
if (!isset($ignored_types)) {
$ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token'));
}
@@ -1156,7 +1436,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach (element_children($element) as $key) {
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key]);
$this->entityFormSharedElements($element[$key], $access);
}
else {
// Ignore non-widget form elements.
@@ -1183,7 +1463,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function entityFormLanguageWidget(&$form, &$form_state) {
if (entity_translation_enabled($this->entityType, $this->bundle)) {
$is_new = $this->isNewEntity();
$is_translation = !$is_new && !empty($form_state['entity_translation']['is_translation']);
$is_translation = !empty($form_state['entity_translation']['is_translation']);
$translations = $this->getTranslations();
$settings = entity_translation_settings($this->entityType, $this->bundle);
$languages = entity_translation_languages($this->entityType, $this->entity);
@@ -1191,8 +1471,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach ($languages as $langcode => $language) {
// Disable languages for existing translations, so it is not possible to
// switch this entity to some language which is already in the translation
// set.
// switch this entity to some language which is already in the
// translation set.
if (!isset($translations->data[$langcode]) || empty($translations->data[$langcode]['source'])) {
$options[$langcode] = t($language->name);
}
@@ -1217,17 +1497,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
if (!empty($form['actions']['submit']['#submit'])) {
$submit = &$form['actions']['submit']['#submit'];
}
else {
if (!isset($form['#submit'])) {
$form['#submit'] = array();
}
$submit = &$form['#submit'];
}
array_unshift($submit, 'entity_translation_language_widget_submit');
// Prepend an empty form element to the form array so that we can update the
// form language before any other form handler has been called.
$form = array(
'entity_translation_entity_form_language_update' => array(
'#element_validate' => array('entity_translation_entity_form_language_update'),
'#entity_type' => $this->entityType,
),
) + $form;
}
/**
@@ -1241,6 +1518,15 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::entityFormValidate()
*/
public function entityFormValidate($form, &$form_state) {
// The locale module is creating the node object anew based on the form's
// submitted values, thus losing the relation with the currently assigned
// translation handler. By adding the 'entity_translation_handler_id' property to
// the $form_state['values'] array, we can persist that relation and utilize
// a correctly instantiated translation handler for the new node entity.
if (empty($form_state['values']['entity_translation_handler_id']) && field_has_translation_handler($this->getEntityType(), 'locale')) {
$form_state['values']['entity_translation_handler_id'] = $form_state[$this->getEntityType()]->entity_translation_handler_id;
}
if (!empty($form_state['values']['translation'])) {
$values = $form_state['values']['translation'];
// Validate the "authored by" field.
@@ -1260,28 +1546,36 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected function updateFormLanguage($form_state) {
// Update the form language as it might have changed. We exploit the
// validation phase to be sure to act as early as possible.
if (isset($form_state['values']['language']) && !$this->isTranslationForm()) {
$this->setFormLanguage($form_state['values'][$this->getLanguageKey()]);
$language_key = $this->getLanguageKey();
if (isset($form_state['values'][$language_key]) && !$this->isTranslationForm()) {
$langcode = $form_state['values'][$language_key];
$this->setActiveLanguage($langcode);
}
}
/**
* @see EntityTranslationHandlerInterface::entityFormLanguageWidgetSubmit()
*/
function entityFormLanguageWidgetSubmit($form, &$form_state) {
public function entityFormLanguageWidgetSubmit($form, &$form_state) {
if (!entity_translation_enabled($this->entityType, $this->bundle)) {
return;
}
$this->updateFormLanguage($form_state);
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
foreach (field_info_instances($this->entityType, $this->bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$previous_langcode = $form[$field_name]['#language'];
if (isset($form[$field_name]['#language'])) {
$field = field_info_field($field_name);
$previous_langcode = $form[$field_name]['#language'];
// Handle a possible language change: new language values are inserted,
// previous ones are deleted.
if ($field['translatable'] && $previous_langcode != $form_langcode) {
$form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
$form_state['values'][$field_name][$previous_langcode] = array();
// Handle a possible language change: new language values are inserted,
// previous ones are deleted.
if ($field['translatable'] && $previous_langcode != $form_langcode && isset($form_state['values'][$field_name][$previous_langcode])) {
$form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
$form_state['values'][$field_name][$previous_langcode] = array();
}
}
}
}
@@ -1290,7 +1584,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::entityFormSubmit()
*/
public function entityFormSubmit($form, &$form_state) {
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$translations = $this->getTranslations();
$is_translation = !empty($form_state['entity_translation']['is_translation']);
$new_translation = !isset($translations->data[$form_langcode]);
@@ -1345,7 +1639,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if (count($translations->data) > 0) {
$languages = language_list();
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$language_tabs = array();
if ($this->getSourceLanguage()) {
@@ -1406,16 +1700,19 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
/**
* Returns the title to be used for the entity form page.
*
* This may contain HTML markup and thus needs to be sanitized, since it will
* be used as page title as-is.
*/
protected function entityFormTitle() {
return $this->getLabel();
return check_plain($this->getLabel());
}
/**
* Returns TRUE if an entity translation is being edited.
*/
protected function isTranslationForm() {
return !$this->isNewEntity() && $this->getFormLanguage() != $this->getLanguage();
return !$this->isNewEntity() && $this->getActiveLanguage() != $this->getLanguage();
}
/**
@@ -1444,7 +1741,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*
* @throws Exception
*/
private function initPathVariables() {
protected function initPathVariables() {
if (empty($this->pathScheme) || !isset($this->entityInfo['translation']['entity_translation']['path schemes'][$this->pathScheme])) {
throw new Exception("Cannot initialize entity translation path variables (invalid path scheme).");
}
@@ -1473,7 +1770,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if ($segment == $this->pathWildcard) {
$path_segments[$index] = $this->getEntityId();
}
elseif ($segment{0} == '%' && isset($this->routerMap[$index])) {
elseif ($segment[0] == '%' && isset($this->routerMap[$index])) {
$path_segments[$index] = $this->routerMap[$index];
}
}
@@ -92,7 +92,9 @@ class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler {
if (!$this->isTranslationForm()) {
// Inherit entity authoring information for the original values.
$values['name'] = $form_state['values']['name'];
if (isset($form_state['values']['name'])) {
$values['name'] = $form_state['values']['name'];
}
if (!empty($form_state['values']['date'])) {
$values['created'] = $form_state['values']['date'];
}
@@ -15,6 +15,24 @@ class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandl
parent::__construct('taxonomy_term', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::getLanguage()
*/
public function getLanguage() {
if (isset($this->entity->vid) && module_exists('i18n_taxonomy')) {
$mode = i18n_taxonomy_vocabulary_mode($this->entity->vid);
// We support also terms having no translation enabled, since they can
// just be language-aware.
if ($mode == I18N_MODE_NONE || $mode == I18N_MODE_ENTITY_TRANSLATION) {
$translations = $this->getTranslations();
if (!empty($translations->original)) {
return $translations->original;
}
}
}
return parent::getLanguage();
}
/**
* @see EntityTranslationDefaultHandler::entityForm()
*/
@@ -32,4 +50,5 @@ class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandl
);
}
}
}
@@ -0,0 +1,137 @@
<?php
/**
* @file
* Translation handler factory for the Entity Translation module.
*/
/**
* Class implementing the entity translation handler factory.
*/
class EntityTranslationHandlerFactory {
/**
* A singleton instance of the factory.
*
* @var EntityTranslationHandlerFactory
*/
protected static $instance;
/**
* Counter used to generate handler ids for new entities.
*
* @var int
*/
protected static $newId = 1;
/**
* Handlers cache.
*
* @var array
*/
protected $handlers = array();
/**
* The last translation handler retrieved.
*
* @var EntityTranslationHandlerInterface
*/
protected $last;
/**
* An array of translation handler retrieved by type.
*
* @var array
*/
protected $lastByType = array();
/**
* Returns the singleton instance of the translation handler factory.
*
* @return EntityTranslationHandlerFactory
*/
public static function getInstance() {
if (!isset(self::$instance)) {
self::$instance = new EntityTranslationHandlerFactory();
}
return self::$instance;
}
/**
* Prevents the factory from being publicly instantiated.
*/
protected function __construct() {}
/**
* Translation handler factory.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity to be translated. A bundle name may be passed to instantiate
* an empty entity.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
public function getHandler($entity_type, $entity) {
if (is_numeric($entity)) {
$entities = entity_load($entity_type, array($entity));
$entity = reset($entities);
}
elseif (is_string($entity)) {
$entity = entity_create_stub_entity($entity_type, array(NULL, NULL, $entity));
}
$id = $this->getHandlerId($entity_type, $entity);
if (!isset($this->handlers[$entity_type][$id])) {
$entity_info = entity_get_info($entity_type);
$class = $entity_info['translation']['entity_translation']['class'];
$handler = new $class($entity_type, $entity_info, $entity);
$handler->setFactory($this);
$this->handlers[$entity_type][$id] = $handler;
}
$this->last = $this->handlers[$entity_type][$id];
$this->lastByType[$entity_type] = $this->last;
$this->last->setEntity($entity);
return $this->last;
}
/**
* Retrieves the translation handler identifier for the given entity.
*
* @param $entity_type
* The type of the entity the translation handler will wrap.
* @param $entity
* The entity the translation handler will wrap.
*/
public function getHandlerId($entity_type, $entity) {
if (!isset($entity->entity_translation_handler_id)) {
list($id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
$revision_id = isset($revision_id) ? $revision_id : 0;
$bundle = isset($bundle) ? $bundle : $entity_type;
$entity->entity_translation_handler_id = $entity_type . '-' . $bundle . '-' . (!empty($id) ? 'eid-' . $id . '-' . $revision_id : 'new-' . self::$newId++);
}
return $entity->entity_translation_handler_id;
}
/**
* Returns the last translation handler retrieved.
*
* @param $entity_type
* (optional) The entity type of the translation handler. Defaults to the
* last one.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
public function getLastHandler($entity_type = NULL) {
if (isset($entity_type)) {
return isset($this->lastByType[$entity_type]) ? $this->lastByType[$entity_type] : NULL;
}
return $this->last;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @file
* MigrateTranslationEntityHandler class for entity_translation.
*/
/**
* Destination handler implementing translatable fields.
*/
class MigrateTranslationEntityHandler extends MigrateDestinationHandler {
/**
* Registers all entites as handled by this class.
*/
public function __construct() {
$this->registerTypes(array('entity'));
}
/**
* Handles Entity Translations.
*
* @param stdClass $entity
* @param stdClass $sourceRow
*/
public function prepare($entity, stdClass $row) {
$migration = Migration::currentMigration();
$entity_type = $migration->getDestination()->getEntityType();
// Only continue if the entity type + bundle combination is enabled for
// entity_translation.
if (entity_translation_enabled($entity_type, $entity)) {
// Get the entity_translation handler and load any existing translations.
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->loadTranslations();
$original_language = $handler->getLanguage();
// Get the bundle of the entity.
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
// We need to get all of the possible translations to create. So we look
// for any translatable fields.
$translatable_fields = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable']) {
$translatable_fields[] = $field_name;
}
}
// In those fields we look for translated values to build out an array
// of languages we need to set translations for.
$translate_languages = array();
foreach ($translatable_fields as $translatable_field) {
if (!empty($entity->{$translatable_field})) {
$field_languages = array_keys($entity->{$translatable_field});
foreach ($field_languages as $field_language) {
$translate_languages[$field_language] = $field_language;
}
}
}
// Remove the LANGUAGE_NONE results.
unset($translate_languages[LANGUAGE_NONE]);
// Remove the original language.
unset($translate_languages[$original_language]);
// Anything we're left with is a translation that should be set.
foreach ($translate_languages as $translate_language) {
if (!isset($entity->translations->data[$translate_language])) {
// Add the new translation and store it.
$handler->setTranslation(array(
'translate' => 0,
'status' => 1,
'language' => $translate_language,
'source' => $original_language,
'uid' => (empty($entity->uid)) ? 0 : $entity->uid,
'changed' => (empty($entity->changed)) ? REQUEST_TIME : $entity->changed,
'created' => (empty($entity->created)) ? REQUEST_TIME : $entity->created,
));
}
}
}
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file
* Plugin to provide access control on entities based on translations.
*/
/**
* CTools Plugin definition.
*/
$plugin = array(
'title' => t("Entity translation: translation exists"),
'description' => t('Control access by checking if a translation exists.'),
'callback' => 'entity_translation_translation_exists_ctools_access_check',
'default' => array('language' => array()),
'settings form' => 'entity_translation_translation_exists_ctools_access_settings',
'settings form submit' => 'entity_translation_translation_exists_ctools_access_settings_submit',
'summary' => 'entity_translation_translation_exists_ctools_access_summary',
'get child' => 'entity_translation_translation_exists_ctools_access_get_child',
'get children' => 'entity_translation_translation_exists_ctools_access_get_children',
);
/**
* Get a particular instance of this plugin.
*/
function entity_translation_translation_exists_ctools_access_get_child($plugin, $parent, $child) {
$plugins = &drupal_static(__FUNCTION__, array());
if (empty($plugins[$parent . ':' . $child])) {
$plugins[$parent . ':' . $child] = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $child);
}
return $plugins[$parent . ':' . $child];
}
/**
* Get all children of this plugin.
*/
function entity_translation_translation_exists_ctools_access_get_children($plugin, $parent) {
$plugins = &drupal_static(__FUNCTION__, array());
if (!empty($plugins)) {
return $plugins;
}
$entities = entity_get_info();
foreach ($entities as $entity_type => $entity) {
$plugin = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type);
$plugins[$parent . ':' . $entity_type] = $plugin;
}
return $plugins;
}
/**
* Plugin definition for one particular plugin child.
*/
function _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type) {
$entity = entity_get_info($entity_type);
$plugin['title'] = t('@entity: entity translation exists', array('@entity' => $entity['label']));
$plugin['keyword'] = $entity_type;
$plugin['description'] = t('Control access by @entity language', array('@entity' => $entity['label']));
$plugin['name'] = $parent . ':' . $entity_type;
$plugin['required context'] = new ctools_context_required(t(ucfirst($entity_type)), $entity_type);
return $plugin;
}
/**
* Settings form for the 'by node_language' access plugin
*/
function entity_translation_translation_exists_ctools_access_settings($form, &$form_state, $conf) {
$options = array(
ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current content language'),
ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
LANGUAGE_NONE => t('Language neutral'),
);
$options = array_merge($options, locale_language_list());
$form['settings']['language'] = array(
'#title' => t('Language'),
'#type' => 'checkboxes',
'#options' => $options,
'#description' => t('This rule will pass if any of these languages are present.'),
'#default_value' => $conf['language'],
);
return $form;
}
/**
* Grant access based on the which translations are available.
*/
function entity_translation_translation_exists_ctools_access_check($conf, $context) {
// Check that the context exists.
if (empty($context) || empty($context->data)) {
return FALSE;
}
$entity = $context->data;
$handler = entity_translation_get_handler($context->keyword, $entity);
if (!empty($handler)) {
$translations = $handler->getTranslations();
global $language_content;
foreach ($conf['language'] as $lang) {
if ($lang) {
switch ($lang) {
case ENTITY_TRANSLATION_LANGUAGE_CURRENT:
$lang = $language_content->language;
break;
case ENTITY_TRANSLATION_LANGUAGE_DEFAULT:
$lang = language_default('language');
break;
}
if (isset($translations->data[$lang]) && $translations->data[$lang]['status']) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Provide a summary description based upon the checked node_languages.
*/
function entity_translation_translation_exists_ctools_access_summary($conf, $context) {
$languages = array(
ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current site content language'),
ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
LANGUAGE_NONE => t('Language neutral'),
);
$languages = array_merge($languages, locale_language_list());
if (!isset($conf['language'])) {
$conf['language'] = array();
}
$names = array();
foreach (array_filter($conf['language']) as $language) {
$names[] = $languages[$language];
}
if (empty($names)) {
return t('@identifier is in any language', array('@identifier' => $context->identifier));
}
return format_plural(count($names), '@languages translation exists for @identifier', '@languages translations exists for identifier', array('@languages' => implode(', ', $names), '@identifier' => $context->identifier));
}
@@ -4,11 +4,11 @@ core = 7.x
package = Testing
hidden = TRUE
dependencies[] = entity_translation
files[] = entity_translation_test.module
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -5,3 +5,34 @@
* Installation functionality for Entity Translation testing module.
*/
/**
* Implements hook_install().
*/
function entity_translation_test_install() {
// Create a simple text field, attached to taxonomy_terms.
field_info_cache_clear();
$field = array(
'field_name' => 'field_simple_text',
'type' => 'text',
'cardinality' => 1,
);
field_create_field($field);
$instance = array(
'field_name' => $field['field_name'],
'label' => ucfirst(str_replace('_', ' ', $field['field_name'])),
'entity_type' => 'taxonomy_term',
'bundle' => 'tags',
'widget' => array(
'type' => 'text_textfield',
),
'display' => array(
'default' => array(
'type' => 'text_default',
),
),
);
field_create_instance($instance);
}
@@ -5,6 +5,18 @@
* Testing functionality for Entity Translation module.
*/
/**
* Implements hook_form_alter().
*/
function entity_translation_test_form_alter(&$form, &$form_state) {
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
$form_state['no_cache'] = $handler->isNewEntity() && variable_get('entity_translation_test_disable_form_cache', FALSE);
}
}
}
/**
* Implements hook_entity_translation_insert().
*/
@@ -182,7 +182,7 @@ function entity_translation_views_data() {
);
$data['entity_translation']['translation_exists'] = array(
'title' => t('Translation exists'),
'help' => t('Link to translation overview page.'),
'help' => t('Determines if a translation exists for a particular translation.'),
'filter' => array(
'handler' => 'entity_translation_handler_filter_translation_exists',
),
@@ -224,4 +224,24 @@ function entity_translation_views_data_alter(&$data) {
);
}
}
// Expose all translatable fields, using a handler based off Views' default
// field handler, that allows users to select the language to display the
// field in.
foreach (field_info_fields() as $field) {
if ($field['translatable'] && $field['storage']['type'] == 'field_sql_storage') {
// Set defaults and just change the handler, title, group, and help.
$defaults = field_views_field_default_views_data($field);
foreach ($defaults as $table_name => $table_data) {
if (isset($data[$table_name][$field['field_name']]) && isset($table_data[$field['field_name']])) {
$field_title = $table_data[$field['field_name']]['title'];
$table_data[$field['field_name']]['title'] = t('!title: translated', array('!title' => $field_title));
$table_data[$field['field_name']]['group'] = t('Entity translation');
$table_data[$field['field_name']]['help'] .= ' ' . t('Show the field !title translated into a specified language', array('!title' => $field_title));
$table_data[$field['field_name']]['field']['handler'] = 'entity_translation_handler_field_field';
$data[$table_name][$field['field_name'] . '_et'] = $table_data[$field['field_name']];
}
}
}
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @file
* This file contains a field handler for entity translation that shows field
* content translated into a specified language.
*/
class entity_translation_handler_field_field extends views_handler_field_field {
function option_definition() {
$options = parent::option_definition();
$options['language'] = array(
'default' => '***CURRENT_LANGUAGE***',
);
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$languages = array(
'***CURRENT_LANGUAGE***' => t("Current user's language"),
'***DEFAULT_LANGUAGE***' => t("Default site language"),
LANGUAGE_NONE => t('Language neutral'),
);
$languages = array_merge($languages, locale_language_list());
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $languages,
'#default_value' => $this->options['language'],
'#description' => t('Select the language to display this field in')
);
}
/**
* Overrides parent::field_language, retrieving the language from the handler
* options.
*/
function field_language($entity_type, $entity) {
global $language_content;
if (field_is_translatable($entity_type, $this->field_info)) {
$default_language = language_default('language');
$language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'),
array($language_content->language, $default_language),
$this->options['language']);
// Give the Field Language API a chance to fallback to a different language
// (or LANGUAGE_NONE), in case the field has no data for the selected language.
// field_view_field() does this as well, but since the returned language code
// is used before calling it, the fallback needs to happen explicitly.
$language = field_language($entity_type, $entity, $this->field_info['field_name'], $language);
return $language;
}
else {
return LANGUAGE_NONE;
}
}
}
@@ -1,7 +1,8 @@
<?php
/**
* @file
* Contains the relationship plugin for relating entities to translation metadata.
* Contains a views plugin for relating entities to translation metadata.
*/
/**
@@ -27,7 +28,10 @@ class entity_translation_handler_relationship extends views_handler_relationship
$alias = $def['table'] . '_' . $this->table;
// We need to add a condition on entity type to the join to avoid getting
// relationships to entities with other types.
$join->extra = "$alias.entity_type = '{$def['entity type']}'";
$join->extra = array(
array('field' => 'entity_type', 'value' => $def['entity type']),
);
$this->alias = $this->query->add_relationship($alias, $join, 'entity_translation', $this->relationship);
}
}