content_translation.admin.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <?php
  2. /**
  3. * @file
  4. * The content translation administration forms.
  5. */
  6. use Drupal\content_translation\BundleTranslationSettingsInterface;
  7. use Drupal\content_translation\ContentTranslationManager;
  8. use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
  9. use Drupal\Core\Entity\ContentEntityTypeInterface;
  10. use Drupal\Core\Entity\EntityTypeInterface;
  11. use Drupal\Core\Field\FieldDefinitionInterface;
  12. use Drupal\Core\Field\FieldStorageDefinitionInterface;
  13. use Drupal\Core\Form\FormStateInterface;
  14. use Drupal\Core\Language\LanguageInterface;
  15. use Drupal\Core\Render\Element;
  16. /**
  17. * Returns a form element to configure field synchronization.
  18. *
  19. * @param \Drupal\Core\Field\FieldDefinitionInterface $field
  20. * A field definition object.
  21. * @param string $element_name
  22. * (optional) The element name, which is added to drupalSettings so that
  23. * javascript can manipulate the form element.
  24. *
  25. * @return array
  26. * A form element to configure field synchronization.
  27. */
  28. function content_translation_field_sync_widget(FieldDefinitionInterface $field, $element_name = 'third_party_settings[content_translation][translation_sync]') {
  29. // No way to store field sync information on this field.
  30. if (!($field instanceof ThirdPartySettingsInterface)) {
  31. return [];
  32. }
  33. $element = [];
  34. $definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
  35. $column_groups = $definition['column_groups'];
  36. if (!empty($column_groups) && count($column_groups) > 1) {
  37. $options = [];
  38. $default = [];
  39. $require_all_groups_for_translation = [];
  40. foreach ($column_groups as $group => $info) {
  41. $options[$group] = $info['label'];
  42. $default[$group] = !empty($info['translatable']) ? $group : FALSE;
  43. if (!empty($info['require_all_groups_for_translation'])) {
  44. $require_all_groups_for_translation[] = $group;
  45. }
  46. }
  47. $default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
  48. $element = [
  49. '#type' => 'checkboxes',
  50. '#title' => t('Translatable elements'),
  51. '#options' => $options,
  52. '#default_value' => $default,
  53. ];
  54. if ($require_all_groups_for_translation) {
  55. // The actual checkboxes are sometimes rendered separately and the parent
  56. // element is ignored. Attach to the first option to ensure that this
  57. // does not get lost.
  58. $element[key($options)]['#attached']['drupalSettings']['contentTranslationDependentOptions'] = [
  59. 'dependent_selectors' => [
  60. $element_name => $require_all_groups_for_translation,
  61. ],
  62. ];
  63. $element[key($options)]['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
  64. }
  65. }
  66. return $element;
  67. }
  68. /**
  69. * (proxied) Implements hook_form_FORM_ID_alter().
  70. */
  71. function _content_translation_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state) {
  72. // Inject into the content language settings the translation settings if the
  73. // user has the required permission.
  74. if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
  75. return;
  76. }
  77. /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
  78. $content_translation_manager = \Drupal::service('content_translation.manager');
  79. $default = $form['entity_types']['#default_value'];
  80. foreach ($default as $entity_type_id => $enabled) {
  81. $default[$entity_type_id] = $enabled || $content_translation_manager->isEnabled($entity_type_id) ? $entity_type_id : FALSE;
  82. }
  83. $form['entity_types']['#default_value'] = $default;
  84. $form['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
  85. $entity_type_manager = \Drupal::entityTypeManager();
  86. /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
  87. $entity_field_manager = \Drupal::service('entity_field.manager');
  88. $bundle_info_service = \Drupal::service('entity_type.bundle.info');
  89. foreach ($form['#labels'] as $entity_type_id => $label) {
  90. $entity_type = $entity_type_manager->getDefinition($entity_type_id);
  91. $storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $entity_field_manager->getFieldStorageDefinitions($entity_type_id) : [];
  92. $entity_type_translatable = $content_translation_manager->isSupported($entity_type_id);
  93. foreach ($bundle_info_service->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
  94. // Here we do not want the widget to be altered and hold also the "Enable
  95. // translation" checkbox, which would be redundant. Hence we add this key
  96. // to be able to skip alterations. Alter the title and display the message
  97. // about UI integration.
  98. $form['settings'][$entity_type_id][$bundle]['settings']['language']['#content_translation_skip_alter'] = TRUE;
  99. if (!$entity_type_translatable) {
  100. $form['settings'][$entity_type_id]['#title'] = t('@label (Translation is not supported).', ['@label' => $entity_type->getLabel()]);
  101. continue;
  102. }
  103. // Displayed the "shared fields widgets" toggle.
  104. if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
  105. $settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
  106. $force_hidden = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
  107. $form['settings'][$entity_type_id][$bundle]['settings']['content_translation']['untranslatable_fields_hide'] = [
  108. '#type' => 'checkbox',
  109. '#title' => t('Hide non translatable fields on translation forms'),
  110. '#default_value' => $force_hidden || !empty($settings['untranslatable_fields_hide']),
  111. '#disabled' => $force_hidden,
  112. '#description' => $force_hidden ? t('Moderated content requires non-translatable fields to be edited in the original language form.') : '',
  113. '#states' => [
  114. 'visible' => [
  115. ':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [
  116. 'checked' => TRUE,
  117. ],
  118. ],
  119. ],
  120. ];
  121. }
  122. $fields = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle);
  123. if ($fields) {
  124. foreach ($fields as $field_name => $definition) {
  125. if ($definition->isComputed() || (!empty($storage_definitions[$field_name]) && _content_translation_is_field_translatability_configurable($entity_type, $storage_definitions[$field_name]))) {
  126. $form['settings'][$entity_type_id][$bundle]['fields'][$field_name] = [
  127. '#label' => $definition->getLabel(),
  128. '#type' => 'checkbox',
  129. '#default_value' => $definition->isTranslatable(),
  130. ];
  131. // Display the column translatability configuration widget.
  132. $column_element = content_translation_field_sync_widget($definition, "settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]");
  133. if ($column_element) {
  134. $form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
  135. }
  136. }
  137. }
  138. if (!empty($form['settings'][$entity_type_id][$bundle]['fields'])) {
  139. // Only show the checkbox to enable translation if the bundles in the
  140. // entity might have fields and if there are fields to translate.
  141. $form['settings'][$entity_type_id][$bundle]['translatable'] = [
  142. '#type' => 'checkbox',
  143. '#default_value' => $content_translation_manager->isEnabled($entity_type_id, $bundle),
  144. ];
  145. }
  146. }
  147. }
  148. }
  149. $form['#validate'][] = 'content_translation_form_language_content_settings_validate';
  150. $form['#submit'][] = 'content_translation_form_language_content_settings_submit';
  151. }
  152. /**
  153. * Checks whether translatability should be configurable for a field.
  154. *
  155. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  156. * The entity type definition.
  157. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
  158. * The field storage definition.
  159. *
  160. * @return bool
  161. * TRUE if field translatability can be configured, FALSE otherwise.
  162. *
  163. * @internal
  164. */
  165. function _content_translation_is_field_translatability_configurable(EntityTypeInterface $entity_type, FieldStorageDefinitionInterface $definition) {
  166. // Allow to configure only fields supporting multilingual storage. We skip our
  167. // own fields as they are always translatable. Additionally we skip a set of
  168. // well-known fields implementing entity system business logic.
  169. return $definition->isTranslatable() &&
  170. $definition->getProvider() != 'content_translation' &&
  171. !in_array($definition->getName(), [$entity_type->getKey('langcode'), $entity_type->getKey('default_langcode'), 'revision_translation_affected']);
  172. }
  173. /**
  174. * (proxied) Implements hook_preprocess_HOOK();
  175. */
  176. function _content_translation_preprocess_language_content_settings_table(&$variables) {
  177. // Alter the 'build' variable injecting the translation settings if the user
  178. // has the required permission.
  179. if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
  180. return;
  181. }
  182. $element = $variables['element'];
  183. $build = &$variables['build'];
  184. array_unshift($build['#header'], ['data' => t('Translatable'), 'class' => ['translatable']]);
  185. $rows = [];
  186. foreach (Element::children($element) as $bundle) {
  187. $field_names = !empty($element[$bundle]['fields']) ? Element::children($element[$bundle]['fields']) : [];
  188. if (!empty($element[$bundle]['translatable'])) {
  189. $checkbox_id = $element[$bundle]['translatable']['#id'];
  190. }
  191. $rows[$bundle] = $build['#rows'][$bundle];
  192. if (!empty($element[$bundle]['translatable'])) {
  193. $translatable = [
  194. 'data' => $element[$bundle]['translatable'],
  195. 'class' => ['translatable'],
  196. ];
  197. array_unshift($rows[$bundle]['data'], $translatable);
  198. $rows[$bundle]['data'][1]['data']['#prefix'] = '<label for="' . $checkbox_id . '">';
  199. }
  200. else {
  201. $translatable = [
  202. 'data' => t('N/A'),
  203. 'class' => ['untranslatable'],
  204. ];
  205. array_unshift($rows[$bundle]['data'], $translatable);
  206. }
  207. foreach ($field_names as $field_name) {
  208. $field_element = &$element[$bundle]['fields'][$field_name];
  209. $rows[] = [
  210. 'data' => [
  211. [
  212. 'data' => \Drupal::service('renderer')->render($field_element),
  213. 'class' => ['translatable'],
  214. ],
  215. [
  216. 'data' => [
  217. '#prefix' => '<label for="' . $field_element['#id'] . '">',
  218. '#suffix' => '</label>',
  219. 'bundle' => [
  220. '#prefix' => '<span class="visually-hidden">',
  221. '#suffix' => '</span> ',
  222. '#plain_text' => $element[$bundle]['settings']['#label'],
  223. ],
  224. 'field' => [
  225. '#plain_text' => $field_element['#label'],
  226. ],
  227. ],
  228. 'class' => ['field'],
  229. ],
  230. [
  231. 'data' => '',
  232. 'class' => ['operations'],
  233. ],
  234. ],
  235. '#field_name' => $field_name,
  236. 'class' => ['field-settings'],
  237. ];
  238. if (!empty($element[$bundle]['columns'][$field_name])) {
  239. $column_element = &$element[$bundle]['columns'][$field_name];
  240. foreach (Element::children($column_element) as $key) {
  241. $column_label = $column_element[$key]['#title'];
  242. unset($column_element[$key]['#title']);
  243. $rows[] = [
  244. 'data' => [
  245. [
  246. 'data' => \Drupal::service('renderer')->render($column_element[$key]),
  247. 'class' => ['translatable'],
  248. ],
  249. [
  250. 'data' => [
  251. '#prefix' => '<label for="' . $column_element[$key]['#id'] . '">',
  252. '#suffix' => '</label>',
  253. 'bundle' => [
  254. '#prefix' => '<span class="visually-hidden">',
  255. '#suffix' => '</span> ',
  256. '#plain_text' => $element[$bundle]['settings']['#label'],
  257. ],
  258. 'field' => [
  259. '#prefix' => '<span class="visually-hidden">',
  260. '#suffix' => '</span> ',
  261. '#plain_text' => $field_element['#label'],
  262. ],
  263. 'columns' => [
  264. '#plain_text' => $column_label,
  265. ],
  266. ],
  267. 'class' => ['column'],
  268. ],
  269. [
  270. 'data' => '',
  271. 'class' => ['operations'],
  272. ],
  273. ],
  274. 'class' => ['column-settings'],
  275. ];
  276. }
  277. }
  278. }
  279. }
  280. $build['#rows'] = $rows;
  281. }
  282. /**
  283. * Form validation handler for content_translation_admin_settings_form().
  284. *
  285. * @see content_translation_admin_settings_form_submit()
  286. */
  287. function content_translation_form_language_content_settings_validate(array $form, FormStateInterface $form_state) {
  288. $settings = &$form_state->getValue('settings');
  289. foreach ($settings as $entity_type => $entity_settings) {
  290. foreach ($entity_settings as $bundle => $bundle_settings) {
  291. if (!empty($bundle_settings['translatable'])) {
  292. $name = "settings][$entity_type][$bundle][translatable";
  293. $translatable_fields = isset($settings[$entity_type][$bundle]['fields']) ? array_filter($settings[$entity_type][$bundle]['fields']) : FALSE;
  294. if (empty($translatable_fields)) {
  295. $t_args = ['%bundle' => $form['settings'][$entity_type][$bundle]['settings']['#label']];
  296. $form_state->setErrorByName($name, t('At least one field needs to be translatable to enable %bundle for translation.', $t_args));
  297. }
  298. $values = $bundle_settings['settings']['language'];
  299. if (empty($values['language_alterable']) && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
  300. foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_LOCKED) as $language) {
  301. $locked_languages[] = $language->getName();
  302. }
  303. $form_state->setErrorByName($name, t('Translation is not supported if language is always one of: @locked_languages', ['@locked_languages' => implode(', ', $locked_languages)]));
  304. }
  305. }
  306. }
  307. }
  308. }
  309. /**
  310. * Form submission handler for content_translation_admin_settings_form().
  311. *
  312. * @see content_translation_admin_settings_form_validate()
  313. */
  314. function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) {
  315. /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
  316. $content_translation_manager = \Drupal::service('content_translation.manager');
  317. $entity_types = $form_state->getValue('entity_types');
  318. $settings = &$form_state->getValue('settings');
  319. // If an entity type is not translatable all its bundles and fields must be
  320. // marked as non-translatable. Similarly, if a bundle is made non-translatable
  321. // all of its fields will be not translatable.
  322. foreach ($settings as $entity_type_id => &$entity_settings) {
  323. foreach ($entity_settings as $bundle => &$bundle_settings) {
  324. $fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
  325. if (!empty($bundle_settings['translatable'])) {
  326. $bundle_settings['translatable'] = $bundle_settings['translatable'] && $entity_types[$entity_type_id];
  327. }
  328. if (!empty($bundle_settings['fields'])) {
  329. foreach ($bundle_settings['fields'] as $field_name => $translatable) {
  330. $translatable = $translatable && $bundle_settings['translatable'];
  331. // If we have column settings and no column is translatable, no point
  332. // in making the field translatable.
  333. if (isset($bundle_settings['columns'][$field_name]) && !array_filter($bundle_settings['columns'][$field_name])) {
  334. $translatable = FALSE;
  335. }
  336. $field_config = $fields[$field_name]->getConfig($bundle);
  337. if ($field_config->isTranslatable() != $translatable) {
  338. $field_config
  339. ->setTranslatable($translatable)
  340. ->save();
  341. }
  342. }
  343. }
  344. if (isset($bundle_settings['translatable'])) {
  345. // Store whether a bundle has translation enabled or not.
  346. $content_translation_manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
  347. // Store any other bundle settings.
  348. if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
  349. $content_translation_manager->setBundleTranslationSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']);
  350. }
  351. // Save translation_sync settings.
  352. if (!empty($bundle_settings['columns'])) {
  353. foreach ($bundle_settings['columns'] as $field_name => $column_settings) {
  354. $field_config = $fields[$field_name]->getConfig($bundle);
  355. if ($field_config->isTranslatable()) {
  356. $field_config->setThirdPartySetting('content_translation', 'translation_sync', $column_settings);
  357. }
  358. // If the field does not have translatable enabled we need to reset
  359. // the sync settings to their defaults.
  360. else {
  361. $field_config->unsetThirdPartySetting('content_translation', 'translation_sync');
  362. }
  363. $field_config->save();
  364. }
  365. }
  366. }
  367. }
  368. }
  369. // Ensure entity and menu router information are correctly rebuilt.
  370. \Drupal::entityTypeManager()->clearCachedDefinitions();
  371. \Drupal::service('router.builder')->setRebuildNeeded();
  372. }