entity_translation.admin.inc 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. <?php
  2. /**
  3. * @file
  4. * The entity translation user interface.
  5. */
  6. /**
  7. * Builder function for the entity translation settings form.
  8. */
  9. function entity_translation_admin_form($form, $form_state) {
  10. $options = array();
  11. $form['locale_field_language_fallback'] = array(
  12. '#type' => 'checkbox',
  13. '#title' => t('Allow language fallback'),
  14. '#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'))),
  15. '#default_value' => variable_get('locale_field_language_fallback', TRUE),
  16. );
  17. $form['entity_translation_languages_enabled'] = array(
  18. '#type' => 'checkbox',
  19. '#title' => t('Use only enabled languages'),
  20. '#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.'),
  21. '#default_value' => variable_get('entity_translation_languages_enabled', FALSE),
  22. );
  23. $form['entity_translation_show_fallback_on_overview_pages'] = array(
  24. '#type' => 'checkbox',
  25. '#title' => t('Show fallback statuses on overview pages'),
  26. '#description' => t('Enable to the show fallback information on the entity overview pages.'),
  27. '#default_value' => variable_get('entity_translation_show_fallback_on_overview_pages', FALSE),
  28. '#states' => array(
  29. 'visible' => array(
  30. ':input[name="locale_field_language_fallback"]' => array('checked' => TRUE),
  31. ),
  32. ),
  33. );
  34. $form['entity_translation_shared_labels'] = array(
  35. '#type' => 'checkbox',
  36. '#title' => t('Display shared labels'),
  37. '#description' => t('Append an "all languages" hint to entity form widgets shared across translations.'),
  38. '#default_value' => variable_get('entity_translation_shared_labels', TRUE),
  39. );
  40. $form['entity_translation_workflow_enabled'] = array(
  41. '#type' => 'checkbox',
  42. '#title' => t('Enable translation workflow permissions'),
  43. '#description' => t('By enabling the translation workflow permissions it will be possible to limit the access to the entity form elements. Once this is active every role previously allowed to access the entity form will need to be granted the <em>Edit original values</em> permission to edit the entity in the original language. Moreover, form elements dealing with values shared across the translations will be visible only to roles having been granted the <em>Edit shared values</em> permission.'),
  44. '#default_value' => entity_translation_workflow_enabled(),
  45. );
  46. $_null = NULL;
  47. list($items,) = menu_router_build();
  48. _entity_translation_validate_path_schemes($_null, FALSE, $items);
  49. foreach (entity_get_info() as $entity_type => $info) {
  50. if ($info['fieldable']) {
  51. $et_info = &$info['translation']['entity_translation'];
  52. _entity_translation_process_path_schemes($entity_type, $et_info);
  53. _entity_translation_validate_path_schemes($et_info['path schemes'], $info['label']);
  54. // Translation can be enabled for the current entity only if it defines at
  55. // least one valid base path.
  56. foreach ($et_info['path schemes'] as $delta => $scheme) {
  57. if (!empty($scheme['base path'])) {
  58. $options[$entity_type] = !empty($info['label']) ? t($info['label']) : $entity_type;
  59. break;
  60. }
  61. }
  62. }
  63. }
  64. // Avoid bloating memory with unused data.
  65. drupal_static_reset('_entity_translation_validate_path_schemes');
  66. $enabled_types = array_filter(variable_get('entity_translation_entity_types', array()));
  67. $form['enabled'] = array(
  68. '#type' => 'fieldset',
  69. '#title' => t('Translatable entity types'),
  70. '#description' => t('Select which entities can be translated.'),
  71. '#collapsible' => TRUE,
  72. '#collapsed' => !empty($enabled_types),
  73. );
  74. $form['enabled']['entity_translation_entity_types'] = array(
  75. '#type' => 'checkboxes',
  76. '#options' => $options,
  77. '#default_value' => $enabled_types,
  78. );
  79. $form['tabs'] = array(
  80. '#type' => 'vertical_tabs',
  81. );
  82. $languages = array(
  83. ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default language'),
  84. ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current language'),
  85. ENTITY_TRANSLATION_LANGUAGE_AUTHOR => t('Author language'),
  86. LANGUAGE_NONE => t('Language neutral'),
  87. );
  88. foreach (entity_translation_languages($entity_type) as $langcode => $language) {
  89. $languages[$langcode] = t($language->name);
  90. }
  91. foreach ($enabled_types as $entity_type) {
  92. $label = $options[$entity_type];
  93. $entity_info = entity_get_info($entity_type);
  94. $bundles = !empty($entity_info['bundles']) ? $entity_info['bundles'] : array($entity_type => array('label' => $label));
  95. $enabled_bundles = 0;
  96. foreach ($bundles as $bundle => $info) {
  97. if (entity_translation_enabled_bundle($entity_type, $bundle)) {
  98. $enabled_bundles++;
  99. $settings = entity_translation_settings($entity_type, $bundle);
  100. $settings_key = 'entity_translation_settings_' . $entity_type . '__' . $bundle;
  101. $form['settings'][$entity_type][$settings_key] = array('#tree' => TRUE);
  102. // If the entity defines no bundle we do not need the fieldset.
  103. if (count($bundles) > 1 || $bundle != $entity_type) {
  104. $form['settings'][$entity_type][$settings_key] += array(
  105. '#type' => 'fieldset',
  106. '#title' => $info['label'],
  107. '#collapsible' => TRUE,
  108. '#collapsed' => TRUE,
  109. );
  110. }
  111. $form['settings'][$entity_type][$settings_key]['default_language'] = array(
  112. '#type' => 'select',
  113. '#title' => t('Default language'),
  114. '#options' => $languages,
  115. '#default_value' => $settings['default_language'],
  116. );
  117. $form['settings'][$entity_type][$settings_key]['hide_language_selector'] = array(
  118. '#type' => 'checkbox',
  119. '#title' => t('Hide language selector'),
  120. '#default_value' => $settings['hide_language_selector'],
  121. );
  122. $form['settings'][$entity_type][$settings_key]['exclude_language_none'] = array(
  123. '#type' => 'checkbox',
  124. '#title' => t('Exclude <em>Language neutral</em> from the available languages'),
  125. '#default_value' => $settings['exclude_language_none'],
  126. );
  127. $form['settings'][$entity_type][$settings_key]['lock_language'] = array(
  128. '#type' => 'checkbox',
  129. '#title' => t('Prevent language from being changed once the entity has been created'),
  130. '#default_value' => $settings['lock_language'],
  131. );
  132. $form['settings'][$entity_type][$settings_key]['shared_fields_original_only'] = array(
  133. '#type' => 'checkbox',
  134. '#title' => t('Hide shared elements on translation forms'),
  135. '#description' => t('Display form elements shared across translations only on entity forms for the original language.'),
  136. '#default_value' => $settings['shared_fields_original_only'],
  137. );
  138. }
  139. }
  140. if ($enabled_bundles > 0) {
  141. $form['settings'][$entity_type][$settings_key]['#collapsed'] = $enabled_bundles > 1;
  142. $form['settings'][$entity_type] += array(
  143. '#type' => 'fieldset',
  144. '#group' => 'tabs',
  145. '#title' => $label,
  146. );
  147. }
  148. }
  149. $form = system_settings_form($form);
  150. // Menu rebuilding needs to be performed after the system settings are saved.
  151. $form['#submit'][] = 'entity_translation_admin_form_submit';
  152. return $form;
  153. }
  154. /**
  155. * Submit handler for the entity translation settings form.
  156. */
  157. function entity_translation_admin_form_submit($form, $form_state) {
  158. // Clear the entity info cache for the new entity translation settings.
  159. entity_info_cache_clear();
  160. menu_rebuild();
  161. }
  162. /**
  163. * Applies the given settings to every defined bundle.
  164. *
  165. * @param $entity_type
  166. * The entity type the settings refer to.
  167. * @param $settings
  168. * (optional) The settings to be applied. Defaults to the entity default
  169. * settings.
  170. */
  171. function entity_translation_settings_init($entity_type, $settings = array()) {
  172. if (entity_translation_enabled($entity_type)) {
  173. $info = entity_get_info($entity_type);
  174. $bundles = !empty($info['bundles']) ? array_keys($info['bundles']) : array($entity_type);
  175. foreach ($bundles as $bundle) {
  176. if (entity_translation_enabled_bundle($entity_type, $bundle)) {
  177. $settings += entity_translation_settings($entity_type, $bundle);
  178. }
  179. }
  180. variable_set('entity_translation_settings_' . $entity_type . '__' . $bundle, $settings);
  181. }
  182. }
  183. /**
  184. * Translations overview page callback.
  185. */
  186. function entity_translation_overview($entity_type, $entity, $callback = NULL) {
  187. // Entity translation and node translation share the same system path.
  188. if ($callback && !entity_translation_enabled($entity_type, $entity)) {
  189. $args = array_slice(func_get_args(), 3);
  190. return entity_translation_overview_callback($callback, $args);
  191. }
  192. $handler = entity_translation_get_handler($entity_type, $entity);
  193. // Ensure $entity holds an entity object and not an id.
  194. $entity = $handler->getEntity();
  195. $handler->initPathScheme();
  196. // Initialize translations if they are empty.
  197. $translations = $handler->getTranslations();
  198. if (empty($translations->original)) {
  199. $handler->initTranslations();
  200. $handler->saveTranslations();
  201. }
  202. // Ensure that we have a coherent status between entity language and field
  203. // languages.
  204. if ($handler->initOriginalTranslation()) {
  205. // FIXME!
  206. entity_translation_entity_save($entity_type, $entity);
  207. }
  208. $header = array(t('Language'), t('Source language'), t('Translation'), t('Status'), t('Operations'));
  209. $languages = entity_translation_languages();
  210. $source = $translations->original;
  211. $path = $handler->getViewPath();
  212. $rows = array();
  213. if (drupal_multilingual()) {
  214. // If we have a view path defined for the current entity get the switch
  215. // links based on it.
  216. if ($path) {
  217. $links = EntityTranslationDefaultHandler::languageSwitchLinks($path);
  218. }
  219. $show_fallback = variable_get('locale_field_language_fallback', TRUE) && variable_get('entity_translation_show_fallback_on_overview_pages', FALSE);
  220. foreach ($languages as $language) {
  221. $classes = array();
  222. $options = array();
  223. $language_name = $language->name;
  224. $langcode = $language->language;
  225. $edit_path = $handler->getEditPath($langcode);
  226. $add_path = "{$handler->getEditPath()}/add/$source/$langcode";
  227. if ($edit_path) {
  228. $add_links = EntityTranslationDefaultHandler::languageSwitchLinks($add_path);
  229. $edit_links = EntityTranslationDefaultHandler::languageSwitchLinks($edit_path);
  230. }
  231. if (isset($translations->data[$langcode])) {
  232. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  233. // Existing translation in the translation set: display status.
  234. $is_original = $langcode == $translations->original;
  235. $translation = $translations->data[$langcode];
  236. $label = _entity_translation_label($entity_type, $entity, $langcode);
  237. $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language);
  238. $row_title = l($label, $link['href'], $link);
  239. if (empty($link['href'])) {
  240. $row_title = $is_original ? $label : t('n/a');
  241. }
  242. if ($edit_path && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
  243. $link = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language);
  244. $link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
  245. $options[] = l(t('edit'), $link['href'], $link);
  246. }
  247. $status = $translation['status'] ? t('Published') : t('Not published');
  248. $classes[] = $translation['status'] ? 'published' : 'not-published';
  249. $status .= isset($translation['translate']) && $translation['translate'] ? theme('entity_translation_overview_outdated', array('message' => t('outdated'))) : '';
  250. $classes[] = isset($translation['translate']) && $translation['translate'] ? 'outdated' : '';
  251. if ($is_original) {
  252. $language_name = t('<strong>@language_name</strong>', array('@language_name' => $language_name));
  253. $source_name = t('(original content)');
  254. }
  255. else {
  256. $source_name = $languages[$translation['source']]->name;
  257. }
  258. }
  259. else {
  260. // No such translation in the set yet: help user to create it.
  261. $row_title = $source_name = t('n/a');
  262. if ($source != $langcode && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
  263. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  264. $translatable = FALSE;
  265. foreach (field_info_instances($entity_type, $bundle) as $instance) {
  266. $field_name = $instance['field_name'];
  267. $field = field_info_field($field_name);
  268. if ($field['translatable']) {
  269. $translatable = TRUE;
  270. break;
  271. }
  272. }
  273. $link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language);
  274. $link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
  275. $options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields');
  276. $classes[] = $translatable ? '' : 'non-translatable';
  277. }
  278. $status = t('Not translated');
  279. // Show fallback information if required.
  280. if ($show_fallback) {
  281. $language_fallback_candidates = _entity_translation_language_fallback_get_candidates($language);
  282. $fallback_candidates = array_intersect_key(drupal_map_assoc($language_fallback_candidates), $translations->data);
  283. $fallback_langcode = reset($fallback_candidates);
  284. if ($fallback_langcode !== FALSE) {
  285. $status = t('Fallback from @language', array('@language' => $languages[$fallback_langcode]->name));
  286. $label = _entity_translation_label($entity_type, $entity, $fallback_langcode);
  287. $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $langcode);
  288. $row_title = l($label, $link['href'], $link);
  289. }
  290. }
  291. }
  292. $rows[] = array(
  293. 'data' => array($language_name, $source_name, $row_title, $status, implode(" | ", $options)),
  294. 'class' => $classes,
  295. );
  296. }
  297. }
  298. drupal_set_title(t('Translations of %label', array('%label' => $handler->getLabel())), PASS_THROUGH);
  299. // Add metadata to the build render allow to let other modules know about
  300. // which entity this is.
  301. $build['#entity_type'] = $entity_type;
  302. $build['#entity'] = $entity;
  303. $build['entity_translation_overview'] = array(
  304. '#theme' => 'entity_translation_overview',
  305. '#header' => $header,
  306. '#rows' => $rows,
  307. );
  308. return $build;
  309. }
  310. /**
  311. * Calls the appropriate translation overview callback.
  312. */
  313. function entity_translation_overview_callback($callback, $args) {
  314. if (module_exists($callback['module'])) {
  315. if (isset($callback['file'])) {
  316. $path = isset($callback['file path']) ? $callback['file path'] : drupal_get_path('module', $callback['module']);
  317. require_once DRUPAL_ROOT . '/' . $path . '/' . $callback['file'];
  318. }
  319. return call_user_func_array($callback['page callback'], $args);
  320. }
  321. }
  322. /**
  323. * Returns the appropriate entity label for the given language.
  324. */
  325. function _entity_translation_label($entity_type, $entity, $langcode = NULL) {
  326. if (function_exists('title_entity_label')) {
  327. list (, , $bundle) = entity_extract_ids($entity_type, $entity);
  328. $entity_info = entity_get_info($entity_type);
  329. if (!empty($entity_info['entity keys']['label'])) {
  330. $legacy_field = $entity_info['entity keys']['label'];
  331. if (title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
  332. $title = title_entity_label($entity, $entity_type, $langcode);
  333. if (!empty($title)) {
  334. return $title;
  335. }
  336. }
  337. }
  338. }
  339. return t('view');
  340. }
  341. /**
  342. * Theme wrapper for the entity translation language page.
  343. */
  344. function theme_entity_translation_overview($variables) {
  345. $rows = $variables['rows'];
  346. return theme('table', array(
  347. 'rows' => $rows,
  348. 'header' => $variables['header'],
  349. ));
  350. }
  351. /**
  352. * Theme wrapper for the entity translation language outdated translation.
  353. */
  354. function theme_entity_translation_overview_outdated($variables) {
  355. $message = $variables['message'];
  356. return ' - <span class="marker">' . $message . '</span>';
  357. }
  358. /**
  359. * Translation deletion confirmation form.
  360. */
  361. function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) {
  362. $handler = entity_translation_get_handler($entity_type, $entity);
  363. $handler->setActiveLanguage($langcode);
  364. $handler->initPathScheme();
  365. $languages = language_list();
  366. $form = array(
  367. '#handler' => $handler,
  368. '#entity_type' => $entity_type,
  369. // Ensure $entity holds an entity object and not an id.
  370. '#entity' => $handler->getEntity(),
  371. '#language' => $langcode,
  372. );
  373. return confirm_form(
  374. $form,
  375. t('Are you sure you want to delete the @language translation of %label?', array('@language' => $languages[$langcode]->name, '%label' => $handler->getLabel())),
  376. $handler->getEditPath($langcode),
  377. t('This action cannot be undone.'),
  378. t('Delete'),
  379. t('Cancel')
  380. );
  381. }
  382. /**
  383. * Submit handler for the translation deletion confirmation.
  384. */
  385. function entity_translation_delete_confirm_submit($form, &$form_state) {
  386. $handler = $form['#handler'];
  387. $entity_type = $form['#entity_type'];
  388. $entity = $form['#entity'];
  389. $langcode = $form['#language'];
  390. // Some modules expect the original values to be present when updating the
  391. // field values. Since we are deleting the translation no value has changed.
  392. $entity->original = $entity;
  393. // Remove the translation entry and the related fields.
  394. $handler->removeTranslation($langcode);
  395. entity_translation_entity_save($entity_type, $entity);
  396. $form_state['redirect'] = $handler->getTranslatePath();
  397. }
  398. /**
  399. * Confirm form for changing field translatability.
  400. */
  401. function entity_translation_translatable_form($form, &$form_state, $field_name) {
  402. $field = field_info_field($field_name);
  403. if ($field['translatable']) {
  404. $title = t('Are you sure you want to disable translation for this field?');
  405. $text = t('All occurrences of this field will become <em>untranslatable</em>:');
  406. if (empty($form_state['input'])) {
  407. drupal_set_message(t('All the existing field translations will be deleted. This operation cannot be undone.'), 'warning');
  408. }
  409. }
  410. else {
  411. $title = t('Are you sure you want to enable translation for this field?');
  412. $text = t('All occurrences of this field will become <em>translatable</em>:');
  413. $form['options'] = array(
  414. '#type' => 'fieldset',
  415. '#title' => t('Migration settings'),
  416. '#weight' => -10,
  417. );
  418. $form['options']['copy_all_languages'] = array(
  419. '#title' => t('Copy translations'),
  420. '#description' => t('Copy field data into <em>all</em> existing translations, otherwise data will only be available in the original language.'),
  421. '#type' => 'checkbox',
  422. '#default_value' => TRUE,
  423. );
  424. }
  425. $text .= _entity_translation_field_desc($field);
  426. $text .= t('This operation may take a long time to complete.');
  427. // We need to keep some information for later processing.
  428. $form_state['field'] = $field;
  429. // Store the 'translatable' status on the client side to prevent outdated form
  430. // submits from toggling translatability.
  431. $form['translatable'] = array(
  432. '#type' => 'hidden',
  433. '#default_value' => $field['translatable'],
  434. );
  435. return confirm_form($form, $title, '', $text);
  436. }
  437. /**
  438. * Submit handler for the field settings form.
  439. *
  440. * This submit handler maintains consistency between the translatability of an
  441. * entity and the language under which the field data is stored. When a field is
  442. * marked as translatable, all the data in $entity->{field_name}[LANGUAGE_NONE]
  443. * is moved to $entity->{field_name}[$entity_language]. When a field is marked
  444. * as untranslatable the opposite process occurs. Note that marking a field as
  445. * untranslatable will cause all of its translations to be permanently removed,
  446. * with the exception of the one corresponding to the entity language.
  447. */
  448. function entity_translation_translatable_form_submit($form, $form_state) {
  449. // This is the current state that we want to reverse.
  450. $translatable = $form_state['values']['translatable'];
  451. $field_name = $form_state['field']['field_name'];
  452. $copy_all_languages = !empty($form_state['values']['copy_all_languages']);
  453. $field = field_info_field($field_name);
  454. if ($field['translatable'] !== $translatable) {
  455. // Field translatability has changed since form creation, abort.
  456. $t_args = array('%field_name' => $field_name, '!translatable' => $translatable ? t('untranslatable') : t('translatable'));
  457. drupal_set_message(t('The field %field_name is already !translatable. No change was performed.', $t_args), 'warning');
  458. return;
  459. }
  460. // If a field is untranslatable, it can have no data except under
  461. // LANGUAGE_NONE. Thus we need a field to be translatable before we convert
  462. // data to the entity language. Conversely we need to switch data back to
  463. // LANGUAGE_NONE before making a field untranslatable lest we lose
  464. // information.
  465. $operations = array(
  466. array('entity_translation_translatable_batch', array(!$translatable, $field_name, $copy_all_languages)),
  467. array('entity_translation_translatable_switch', array(!$translatable, $field_name)),
  468. );
  469. $operations = $translatable ? $operations : array_reverse($operations);
  470. $t_args = array('%field' => $field_name);
  471. $title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args);
  472. $batch = array(
  473. 'title' => $title,
  474. 'operations' => $operations,
  475. 'finished' => 'entity_translation_translatable_batch_done',
  476. 'file' => drupal_get_path('module', 'entity_translation') . '/entity_translation.admin.inc',
  477. );
  478. batch_set($batch);
  479. }
  480. /**
  481. * Toggle translatability of the given field.
  482. *
  483. * This is called from a batch operation, but should only run once per field.
  484. */
  485. function entity_translation_translatable_switch($translatable, $field_name) {
  486. $field = field_info_field($field_name);
  487. if ($field['translatable'] === $translatable) {
  488. return;
  489. }
  490. $field['translatable'] = $translatable;
  491. field_update_field($field);
  492. // This is needed for versions of Drupal core 7.10 and lower.
  493. // See http://drupal.org/node/1380660 for details.
  494. drupal_static_reset('field_available_languages');
  495. }
  496. /**
  497. * Batch operation. Convert field data to or from LANGUAGE_NONE.
  498. */
  499. function entity_translation_translatable_batch($translatable, $field_name, $copy_all_languages, &$context) {
  500. if (empty($context['sandbox'])) {
  501. $context['sandbox']['progress'] = 0;
  502. // How many entities will need processing?
  503. $query = new EntityFieldQuery();
  504. $count = $query
  505. ->fieldCondition($field_name)
  506. ->age(FIELD_LOAD_REVISION)
  507. ->count()
  508. ->execute();
  509. if (intval($count) === 0) {
  510. // Nothing to do.
  511. $context['finished'] = 1;
  512. return;
  513. }
  514. $context['sandbox']['max'] = $count;
  515. }
  516. // Number of entities to be processed for each step.
  517. $limit = variable_get('entity_translation_translatable_batch_limit', 10);
  518. $offset = $context['sandbox']['progress'];
  519. $query = new EntityFieldQuery();
  520. $result = $query
  521. ->fieldCondition($field_name)
  522. ->entityOrderBy('entity_id')
  523. ->age(FIELD_LOAD_REVISION)
  524. ->range($offset, $limit)
  525. ->execute();
  526. foreach ($result as $entity_type => $partial_entities) {
  527. // Load all revisionable entities' revisions.
  528. if (EntityTranslationDefaultHandler::isEntityTypeRevisionable($entity_type)) {
  529. $entities = array();
  530. // Extract the revision identifier from the entity's definition.
  531. $entity_info = entity_get_info($entity_type);
  532. $revision_id_key = $entity_info['entity keys']['revision'];
  533. // Load each revisionable entity's revision using $conditions, which
  534. // should include the revision id information.
  535. foreach ($partial_entities as $partial_entity) {
  536. $conditions = (array) $partial_entity;
  537. $revision_condition = array_intersect_key($conditions, array($revision_id_key => $revision_id_key));
  538. $entity_revisions = entity_load($entity_type, FALSE, $revision_condition);
  539. $entities[] = reset($entity_revisions);
  540. }
  541. }
  542. else {
  543. $entities = entity_load($entity_type, array_keys($partial_entities));
  544. }
  545. foreach ($entities as $entity) {
  546. $context['sandbox']['progress']++;
  547. $handler = entity_translation_get_handler($entity_type, $entity);
  548. $langcode = $handler->getLanguage();
  549. // Skip process for language neutral entities.
  550. if ($langcode == LANGUAGE_NONE) {
  551. continue;
  552. }
  553. // We need a two-steps approach while updating field translations: given
  554. // that field-specific update functions might rely on the stored values to
  555. // perform their processing, see for instance file_field_update(), first
  556. // we need to store the new translations and only after we can remove the
  557. // old ones. Otherwise we might have data loss, since the removal of the
  558. // old translations might occur before the new ones are stored.
  559. if ($translatable && isset($entity->{$field_name}[LANGUAGE_NONE])) {
  560. // If the field is being switched to translatable and has data for
  561. // LANGUAGE_NONE then we need to move the data to the right language.
  562. // In case the 'Copy translations' option was selected, move the
  563. // available LANGUAGE_NONE field data into all existing translation
  564. // languages, otherwise only into the entity's language.
  565. $translations = $handler->getTranslations();
  566. if ($copy_all_languages && !empty($translations->data)) {
  567. foreach ($translations->data as $translation) {
  568. $entity->{$field_name}[$translation['language']] = $entity->{$field_name}[LANGUAGE_NONE];
  569. }
  570. }
  571. else {
  572. $entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NONE];
  573. }
  574. // Store the original value.
  575. _entity_translation_update_field($entity_type, $entity, $field_name);
  576. $entity->{$field_name}[LANGUAGE_NONE] = array();
  577. // Remove the language neutral value.
  578. _entity_translation_update_field($entity_type, $entity, $field_name);
  579. }
  580. elseif (!$translatable && isset($entity->{$field_name}[$langcode])) {
  581. // The field has been marked untranslatable and has data in the entity
  582. // language: we need to move it to LANGUAGE_NONE and drop the other
  583. // translations.
  584. $entity->{$field_name}[LANGUAGE_NONE] = $entity->{$field_name}[$langcode];
  585. // Store the original value.
  586. _entity_translation_update_field($entity_type, $entity, $field_name);
  587. // Remove translations.
  588. foreach ($entity->{$field_name} as $langcode => $items) {
  589. if ($langcode != LANGUAGE_NONE) {
  590. $entity->{$field_name}[$langcode] = array();
  591. }
  592. }
  593. _entity_translation_update_field($entity_type, $entity, $field_name);
  594. }
  595. else {
  596. // No need to save unchanged entities.
  597. continue;
  598. }
  599. }
  600. }
  601. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  602. }
  603. /**
  604. * Stores the given field translations.
  605. */
  606. function _entity_translation_update_field($entity_type, $entity, $field_name) {
  607. $empty = 0;
  608. $field = field_info_field($field_name);
  609. // Ensure that we are trying to store only valid data.
  610. foreach ($entity->{$field_name} as $langcode => $items) {
  611. $entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]);
  612. $empty += empty($entity->{$field_name}[$langcode]);
  613. }
  614. // Save the field value only if there is at least one item available,
  615. // otherwise any stored empty field value would be deleted. If this happens
  616. // the range queries would be messed up.
  617. if ($empty < count($entity->{$field_name})) {
  618. entity_translation_entity_save($entity_type, $entity);
  619. }
  620. }
  621. /**
  622. * Check the exit status of the batch operation.
  623. */
  624. function entity_translation_translatable_batch_done($success, $results, $operations) {
  625. if ($success) {
  626. drupal_set_message(t("Data successfully processed."));
  627. }
  628. else {
  629. // @todo: Do something about this case.
  630. drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."));
  631. }
  632. }
  633. /**
  634. * Returns language fallback candidates for a certain language.
  635. *
  636. * @param object $language
  637. * Drupal language object.
  638. *
  639. * @return array
  640. * An array of language codes in the fallback order.
  641. */
  642. function _entity_translation_language_fallback_get_candidates($language) {
  643. // Save original fallback candidates.
  644. $language_fallback_original = drupal_static('language_fallback_get_candidates');
  645. // Replace all global languages with the given one. We need this because the
  646. // language_fallback_get_candidates() does not take the $language parameter,
  647. // however other modules (like language_fallback) may use current language(s)
  648. // when they alter the language fallback candidates.
  649. $languages_original = array();
  650. foreach (language_types() as $language_type) {
  651. $languages_original[$language_type] = $GLOBALS[$language_type];
  652. $GLOBALS[$language_type] = $language;
  653. }
  654. // Clear static cache, so that fallback candidates are recalculated.
  655. drupal_static_reset('language_fallback_get_candidates');
  656. $language_fallback_candidates = language_fallback_get_candidates();
  657. // Restore original data.
  658. $language_fallback =& drupal_static('language_fallback_get_candidates');
  659. $language_fallback = $language_fallback_original;
  660. foreach ($languages_original as $language_type => $_language) {
  661. $GLOBALS[$language_type] = $_language;
  662. }
  663. return $language_fallback_candidates;
  664. }