content_translation.module 42.4 KB
Newer Older
1
2
3
4
5
6
7
<?php

/**
 * @file
 * Allows entities to be translated into different languages.
 */

8
use Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks;
9
use Drupal\Core\Entity\ContentEntityInterface;
10
11
use Drupal\Core\Entity\EntityFormControllerInterface;
use Drupal\Core\Entity\EntityInterface;
12
13
14
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TranslatableInterface;
15
use Drupal\node\NodeInterface;
16
17
18
19

/**
 * Implements hook_help().
 */
20
function content_translation_help($path, $arg) {
21
  switch ($path) {
22
    case 'admin/help#content_translation':
23
24
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
25
      $output .= '<p>' . t('The Content Translation module allows you to create and manage translations for your Drupal site content. You can specify which elements need to be translated at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may provide additional elements that can be translated. For more information, see the online handbook entry for <a href="!url">Content Translation</a>.', array('!url' => 'http://drupal.org/documentation/modules/translation_entity')) . '</p>';
26
27
28
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Enabling translation') . '</dt>';
29
      $output .= '<dd><p>' . t('Before you can translate content, there must be at least two languages added on the <a href="!url">languages administration</a> page.', array('!url' => url('admin/config/regional/language'))) . '</p>';
30
      $output .= '<p>' . t('After adding languages, <a href="!url">configure translation</a>.', array('!url' => url('admin/config/regional/content-language'))) . '</p>';
31
      $output .= '<dt>' . t('Translating content') . '</dt>';
32
      $output .= '<dd>' . t('After enabling translation you can create a new piece of content, or edit existing content and assign it a language. Then, you will see a <em>Translate</em> tab or link that will gives an overview of the translation status for the current content. From there, you can add translations and edit or delete existing translations. This process is similar for every translatable element on your site, such as taxonomy terms, comments or user accounts.') . '</dd>';
33
34
35
36
37
      $output .= '<dt>' . t('Changing source language') . '</dt>';
      $output .= '<dd>' . t('When there are two or more possible source languages, selecting a <em>Source language</em> will repopulate the form using the specified source\'s values. For example, French is much closer to Spanish than to Chinese, so changing the French translation\'s source language to Spanish can assist translators.') . '</dd>';
      $output .= '<dt>' . t('Maintaining translations') . '</dt>';
      $output .= '<dd>' . t('If editing content in one language requires that translated versions also be updated to reflect the change, use the <em>Flag other translations as outdated</em> check box to mark the translations as outdated and in need of revision.') . '</dd>';
      $output .= '<dt>' . t('Translation permissions') . '</dt>';
38
      $output .= '<dd>' . t('The Content Translation module makes a basic set of permissions available. Additional <a href="@permissions">permissions</a> are made available after translation is enabled for each translatable element.', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-content_translation')))) . '</dd>';
39
40
      $output .= '</dl>';
      return $output;
41
42
43

    case 'admin/config/regional/content-language':
      $output = '';
44
      if (!\Drupal::languageManager()->isMultilingual()) {
45
        $output .= '<br/>' . t('Before you can translate content, there must be at least two languages added on the <a href="!url">languages administration</a> page.', array('!url' => url('admin/config/regional/language')));
46
47
      }
      return $output;
48
49
50
  }
}

51
52
53
/**
 * Implements hook_module_implements_alter().
 */
54
function content_translation_module_implements_alter(&$implementations, $hook) {
55
56
57
58
  switch ($hook) {
    // Move some of our hook implementations to the end of the list.
    case 'menu_alter':
    case 'entity_info_alter':
59
60
61
      $group = $implementations['content_translation'];
      unset($implementations['content_translation']);
      $implementations['content_translation'] = $group;
62
63
64
65
      break;
  }
}

66
67
68
/**
 * Implements hook_language_type_info_alter().
 */
69
function content_translation_language_types_info_alter(array &$language_types) {
70
71
72
  // Make content language negotiation configurable by removing the 'locked'
  // flag.
  $language_types[Language::TYPE_CONTENT]['locked'] = FALSE;
73
  unset($language_types[Language::TYPE_CONTENT]['fixed']);
74
75
76
77
}

/**
 * Implements hook_entity_info_alter().
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
 *
 * The content translation UI relies on the entity info to provide its features.
 * See the documentation of hook_entity_info() in the Entity API documentation
 * for more details on all the entity info keys that may be defined.
 *
 * To make Content Translation automatically support an entity type some keys
 * may need to be defined, but none of them is required unless the entity path
 * is different from the usual /ENTITY_TYPE/{ENTITY_TYPE} pattern (for instance
 * "/taxonomy/term/{taxonomy_term}"), in which case at least the 'canonical' key
 * in the 'links' entity info property must be defined.
 *
 * Every entity type needs a translation controller to be translated. This can
 * be specified through the 'translation' key in the 'controllers' entity info
 * property. If an entity type is translatable and no translation controller is
 * defined, \Drupal\content_translation\ContentTranslationController will be
 * assumed. Every translation controller class must implement
 * \Drupal\content_translation\ContentTranslationControllerInterface.
 *
 * If the entity paths match the default pattern above and there is no need for
 * an entity-specific translation controller class, Content Translation will
 * provide built-in support for the entity. However enabling translation for
 * each translatable bundle will be required.
 *
 * @see \Drupal\Core\Entity\Annotation\EntityType
102
 */
103
function content_translation_entity_info_alter(array &$entity_info) {
104
  // Provide defaults for translation info.
105
  /** @var $entity_info \Drupal\Core\Entity\EntityTypeInterface[] */
106
  foreach ($entity_info as $entity_type => &$info) {
107
    if ($info->isTranslatable()) {
108
109
      if (!$info->hasControllerClass('translation')) {
        $info->setControllerClass('translation', 'Drupal\content_translation\ContentTranslationController');
110
      }
111

112
113
114
      $translation = $info->get('translation');
      if (!$translation || !isset($translation['content_translation'])) {
        $translation['content_translation'] = array();
115
      }
116

117
      if ($info->hasLinkTemplate('canonical')) {
118
        // Provide default route names for the translation paths.
119
120
121
        if (!$info->hasLinkTemplate('drupal:content-translation-overview')) {
          $info->setLinkTemplate('drupal:content-translation-overview', "content_translation.translation_overview_$entity_type");
        }
122
123
        // @todo Remove this as soon as menu access checks rely on the
        //   controller. See https://drupal.org/node/2155787.
124
        $translation['content_translation'] += array(
125
          'access_callback' => 'content_translation_translate_access',
126
127
        );
      }
128
      $info->set('translation', $translation);
129
    }
130
131
  }
}
132

133
134
135
/**
 * Implements hook_entity_bundle_info_alter().
 */
136
function content_translation_entity_bundle_info_alter(&$bundles) {
137
138
  foreach ($bundles as $entity_type => &$info) {
    foreach ($info as $bundle => &$bundle_info) {
139
      $enabled = content_translation_get_config($entity_type, $bundle, 'enabled');
140
      $bundle_info['translatable'] = !empty($enabled);
141
142
143
144
    }
  }
}

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * Implements hook_entity_field_info_alter().
 */
function content_translation_entity_field_info_alter(&$info, $entity_type) {
  $translation_settings = config('content_translation.settings')->get($entity_type);

  if ($translation_settings) {
    // Currently field translatability is defined per-field but we may want to
    // make it per-instance instead, so leaving the possibility open for further
    // easier refactoring.
    $fields = array();
    foreach ($translation_settings as $bundle => $settings) {
      $fields += !empty($settings['content_translation']['fields']) ? $settings['content_translation']['fields'] : array();
    }

    $keys = array('definitions', 'optional');
    foreach ($fields as $name => $translatable) {
      foreach ($keys as $key) {
        if (isset($info[$key][$name])) {
164
          $info[$key][$name]->setTranslatable((bool) $translatable);
165
166
167
168
169
170
171
          break;
        }
      }
    }
  }
}

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
 * Implements hook_entity_operation_alter().
 */
function content_translation_entity_operation_alter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity) {
  // @todo Use an access permission.
  if ($entity instanceof NodeInterface && $entity->isTranslatable()) {
    $uri = $entity->uri();
    $operations['translate'] = array(
      'title' => t('Translate'),
      'href' => $uri['path'] . '/translations',
      'options' => $uri['options'],
    );
  }
}

187
188
/**
 * Implements hook_menu().
189
190
191
 *
 * @todo Split this into route definition and menu link definition. See
 *   https://drupal.org/node/1987882 and https://drupal.org/node/2047633.
192
 */
193
function content_translation_menu() {
194
195
196
  $items = array();

  // Create tabs for all possible entity types.
197
  foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $info) {
198
    // Provide the translation UI only for enabled types.
199
    if (content_translation_enabled($entity_type)) {
200
      $path = _content_translation_link_to_router_path($entity_type, $info->getLinkTemplate('canonical'));
201
      $entity_position = count(explode('/', $path)) - 1;
202
      $keys = array_flip(array('load_arguments'));
203
204
      $translation = $info->get('translation');
      $menu_info = array_intersect_key($translation['content_translation'], $keys) + array('file' => 'content_translation.pages.inc');
205
206
207
208
209
210
211
212
213
214
215
216
217
      $item = array();

      // Plugin annotations cannot contain spaces, thus we need to restore them
      // from underscores.
      foreach ($menu_info as $key => $value) {
        $item[str_replace('_', ' ', $key)] = $value;
      }

      // Add translation callback.
      // @todo Add the access callback instead of replacing it as soon as the
      // routing system supports multiple callbacks.
      $language_position = $entity_position + 3;
      $args = array($entity_position, $language_position, $language_position + 1);
218
      $items["$path/translations/add/%language/%language"] = array(
219
        'title' => 'Add',
220
        'route_name' => "content_translation.translation_add_$entity_type",
221
        'weight' => 1,
222
      );
223
224
225
226
227

      // Edit translation callback.
      $args = array($entity_position, $language_position);
      $items["$path/translations/edit/%language"] = array(
        'title' => 'Edit',
228
        'route_name' => "content_translation.translation_edit_$entity_type",
229
        'weight' => 1,
230
      );
231
232
233
234

      // Delete translation callback.
      $items["$path/translations/delete/%language"] = array(
        'title' => 'Delete',
235
        'route_name' => "content_translation.delete_$entity_type",
236
237
238
239
240
241
242
243
244
      ) + $item;
    }
  }

  return $items;
}

/**
 * Implements hook_menu_alter().
245
246
247
 *
 * @todo Split this into route definition and menu link definition. See
 *   https://drupal.org/node/1987882 and https://drupal.org/node/2047633.
248
 */
249
function content_translation_menu_alter(array &$items) {
250
251
252
253
  // Clarify where translation settings are located.
  $items['admin/config/regional/content-language']['title'] = 'Content language and translation';
  $items['admin/config/regional/content-language']['description'] = 'Configure language and translation support for content.';

254
  // Check that the declared menu base paths are actually valid.
255
  foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $info) {
256
    if (content_translation_enabled($entity_type)) {
257
      $path = _content_translation_link_to_router_path($entity_type, $info->getLinkTemplate('canonical'));
258

259
260
261
262
      // If the base path is not defined we cannot provide the translation UI
      // for this entity type. In some cases the actual base path might not have
      // a menu loader associated, hence we need to check also for the plain "%"
      // variant. See for instance comment_menu().
263
      if (!isset($items[$path]) && !isset($items["$path/edit"])
264
          && !isset($items[_content_translation_menu_strip_loaders($path)])) {
265
266
        unset(
          $items["$path/translations"],
267
          $items["$path/translations/add/%language/%language"],
268
269
          $items["$path/translations/delete/%language"]
        );
270
        $t_args = array('@entity_type' => $info->getLabel() ?: $entity_type);
271
        watchdog('content translation', 'The entities of type @entity_type do not define a valid base path: it will not be possible to translate them.', $t_args, WATCHDOG_WARNING);
272
273
      }
      else {
274
        $edit_path = $path . '/edit';
275
276
277
278
279
280
281
282
283
284
285

        if (isset($items[$edit_path])) {
          // If the edit path is a default local task we need to find the parent
          // item.
          $edit_path_split = explode('/', $edit_path);
          do {
            $entity_form_item = &$items[implode('/', $edit_path_split)];
            array_pop($edit_path_split);
          }
          while (!empty($entity_form_item['type']) && $entity_form_item['type'] == MENU_DEFAULT_LOCAL_TASK);

286
          // Make the "Translate" tab follow the "Edit" one when possible.
287
288
289
290
291
292
293
294
295
          if (isset($entity_form_item['weight'])) {
            $items["$path/translations"]['weight'] = $entity_form_item['weight'] + 0.01;
          }
        }
      }
    }
  }
}

296
297
298
299
300
301
302
303
304
/**
 * Implements hook_menu_link_defaults_alter().
 */
function content_translation_menu_link_defaults_alter(array &$links) {
  // Clarify where translation settings are located.
  $items['admin.config.regional.language.content_settings_page']['link_title'] = 'Content language and translation';
  $items['admin.config.regional.language.content_settings_page']['description'] = 'Configure language and translation support for content.';
}

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/**
 * Convert an entity canonical link to a router path.
 *
 * @param string $link
 *   The entity link to be converted.
 *
 * @return string
 *   The resulting router path. For instance "/node/{node}" is turned into
 *   "node/%node".
 *
 * @todo Remove this and use the actual link values when all the Content
 *   Translation code is adapted to the new routing system.
 */
function _content_translation_link_to_router_path($entity_type, $link) {
  $path = preg_replace('|{([^}]+)}|', '%$1', trim($link, '/'));
  return str_replace('%id', '%' . $entity_type, $path);
}

323
324
325
326
327
328
329
330
331
/**
 * Strips out menu loaders from the given path.
 *
 * @param string $path
 *   The path to process.
 *
 * @return
 *   The given path where all the menu loaders are replaced with "%".
 */
332
function _content_translation_menu_strip_loaders($path) {
333
334
335
  return preg_replace('|%[^/]+|', '%', $path);
}

336
337
338
339
340
341
/**
 * Access callback for the translation overview page.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity whose translation overview should be displayed.
 */
342
function content_translation_translate_access(EntityInterface $entity) {
343
  return $entity instanceof ContentEntityInterface && empty($entity->getUntranslated()->language()->locked) && \Drupal::languageManager()->isMultilingual() && $entity->isTranslatable() &&
344
    (user_access('create content translations') || user_access('update content translations') || user_access('delete content translations'));
345
346
}

347
348
349
350
351
352
353
/**
 * Checks whether the given user can view the specified translation.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity whose translation overview should be displayed.
 * @param $langcode
 *   The language code of the translation to be displayed.
354
 * @param \Drupal\Core\Session\AccountInterface $account
355
356
357
 *   (optional) The account for which view access should be checked. Defaults to
 *   the current user.
 */
358
function content_translation_view_access(EntityInterface $entity, $langcode, AccountInterface $account = NULL) {
359
  $entity_type = $entity->entityType();
360
361
  $info = $entity->entityInfo();
  $permission = "translate $entity_type";
362
  if ($info->getPermissionGranularity() == 'bundle') {
363
364
365
    $permission = "translate {$entity->bundle()} $entity_type";
  }
  return !empty($entity->translation[$langcode]['status']) || user_access('translate any entity', $account) || user_access($permission, $account);
366
367
}

368
369
370
371
372
373
/**
 * Access callback for the translation addition page.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity being translated.
 * @param \Drupal\Core\Language\Language $source
374
375
 *   (optional) The language of the values being translated. Defaults to the
 *   entity language.
376
 * @param \Drupal\Core\Language\Language $target
377
378
 *   (optional) The language of the translated values. Defaults to the current
 *   content language.
379
 */
380
function content_translation_add_access(EntityInterface $entity, Language $source = NULL, Language $target = NULL) {
381
  $source = !empty($source) ? $source : $entity->language();
382
  $target = !empty($target) ? $target : language(Language::TYPE_CONTENT);
383
384
  $translations = $entity->getTranslationLanguages();
  $languages = language_list();
385
  return $source->id != $target->id && isset($languages[$source->id]) && isset($languages[$target->id]) && !isset($translations[$target->id]) && content_translation_access($entity, 'create');
386
387
388
389
390
391
392
393
394
395
396
}

/**
 * Access callback for the translation edit page.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity being translated.
 * @param \Drupal\Core\Language\Language $language
 *   (optional) The language of the translated values. Defaults to the current
 *   content language.
 */
397
function content_translation_edit_access(EntityInterface $entity, Language $language = NULL) {
398
  $language = !empty($language) ? $language : language(Language::TYPE_CONTENT);
399
400
  $translations = $entity->getTranslationLanguages();
  $languages = language_list();
401
  return isset($languages[$language->id]) && $language->id != $entity->getUntranslated()->language()->id && isset($translations[$language->id]) && content_translation_access($entity, 'update');
402
403
404
405
406
407
408
409
410
411
412
}

/**
 * Access callback for the translation delete page.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity being translated.
 * @param \Drupal\Core\Language\Language $language
 *   (optional) The language of the translated values. Defaults to the current
 *   content language.
 */
413
function content_translation_delete_access(EntityInterface $entity, Language $language = NULL) {
414
  $language = !empty($language) ? $language : language(Language::TYPE_CONTENT);
415
416
  $translations = $entity->getTranslationLanguages();
  $languages = language_list();
417
  return isset($languages[$language->id]) && $language->id != $entity->getUntranslated()->language()->id && isset($translations[$language->id]) && content_translation_access($entity, 'delete');
418
419
}

420
421
422
/**
 * Implements hook_library_info().
 */
423
424
425
426
function content_translation_library_info() {
  $path = drupal_get_path('module', 'content_translation');
  $libraries['drupal.content_translation.admin'] = array(
    'title' => 'Content translation UI',
427
    'version' => \Drupal::VERSION,
428
    'js' => array(
429
      $path . '/content_translation.admin.js' => array(),
430
431
    ),
    'css' => array(
432
      $path . '/css/content_translation.admin.css' => array(),
433
434
435
436
437
438
439
440
441
442
443
    ),
    'dependencies' => array(
      array('system', 'jquery'),
      array('system', 'drupal'),
      array('system', 'jquery.once'),
    ),
  );

  return $libraries;
}

444
/**
445
 * Returns the key name used to store the configuration setting.
446
 *
447
448
 * Based on the entity type and bundle, the keys used to store configuration
 * will have a common root name.
449
450
451
452
453
454
455
456
457
 *
 * @param string $entity_type
 *   The type of the entity the setting refers to.
 * @param string $bundle
 *   The bundle of the entity the setting refers to.
 * @param string $setting
 *   The name of the setting.
 *
 * @return string
458
 *   The key name of the configuration setting.
459
460
461
462
 *
 * @todo Generalize this logic so that it is available to any module needing
 *   per-bundle configuration.
 */
463
function content_translation_get_config_key($entity_type, $bundle, $setting) {
464
465
  $entity_type = preg_replace('/[^0-9a-zA-Z_]/', "_", $entity_type);
  $bundle = preg_replace('/[^0-9a-zA-Z_]/', "_", $bundle);
466
  return $entity_type . '.' . $bundle . '.content_translation.' . $setting;
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
}

/**
 * Retrieves the value for the specified setting.
 *
 * @param string $entity_type
 *   The type of the entity the setting refer to.
 * @param string $bundle
 *   The bundle of the entity the setting refer to.
 * @param string $setting
 *   The name of the setting.
 *
 * @returns mixed
 *   The stored value for the given setting.
 */
482
483
function content_translation_get_config($entity_type, $bundle, $setting) {
  $key = content_translation_get_config_key($entity_type, $bundle, $setting);
484
  return \Drupal::config('content_translation.settings')->get($key);
485
486
487
488
489
490
491
492
493
494
495
496
497
498
}

/**
 * Stores the given value for the specified setting.
 *
 * @param string $entity_type
 *   The type of the entity the setting refer to.
 * @param string $bundle
 *   The bundle of the entity the setting refer to.
 * @param string $setting
 *   The name of the setting.
 * @param $value
 *   The value to be stored for the given setting.
 */
499
500
function content_translation_set_config($entity_type, $bundle, $setting, $value) {
  $key = content_translation_get_config_key($entity_type, $bundle, $setting);
501
  return \Drupal::config('content_translation.settings')->set($key, $value)->save();
502
503
504
505
506
507
508
509
510
511
512
513
514
515
}

/**
 * Determines whether the given entity type is translatable.
 *
 * @param string $entity_type
 *   The type of the entity.
 * @param string $bundle
 *   (optional) The bundle of the entity. If no bundle is provided, all the
 *   available bundles are checked.
 *
 * @returns
 *   TRUE if the specified bundle is translatable. If no bundle is provided
 *   returns TRUE if at least one of the entity bundles is translatable.
516
517
 *
 * @todo Move to \Drupal\content_translation\ContentTranslationManager.
518
 */
519
function content_translation_enabled($entity_type, $bundle = NULL) {
520
521
  $enabled = FALSE;

522
  if (\Drupal::service('content_translation.manager')->isSupported($entity_type)) {
523
524
    $bundles = !empty($bundle) ? array($bundle) : array_keys(entity_get_bundles($entity_type));
    foreach ($bundles as $bundle) {
525
      if (content_translation_get_config($entity_type, $bundle, 'enabled')) {
526
527
528
        $enabled = TRUE;
        break;
      }
529
530
531
    }
  }

532
  return $enabled;
533
534
535
}

/**
536
 * Content translation controller factory.
537
538
539
540
 *
 * @param string $entity_type
 *   The type of the entity being translated.
 *
541
542
 * @return \Drupal\content_translation\ContentTranslationControllerInterface
 *   An instance of the content translation controller interface.
543
544
 *
 * @todo Move to \Drupal\content_translation\ContentTranslationManager.
545
 */
546
function content_translation_controller($entity_type) {
547
  $entity_info = \Drupal::entityManager()->getDefinition($entity_type);
548
  // @todo Throw an exception if the key is missing.
549
  $class = $entity_info->getControllerClass('translation');
550
  return new $class($entity_info);
551
552
553
554
555
556
557
558
559
}

/**
 * Returns the entity form controller for the given form.
 *
 * @param array $form_state
 *   The form state array holding the entity form controller.
 *
 * @return \Drupal\Core\Entity\EntityFormControllerInterface;
560
 *   An instance of the content translation form interface or FALSE if not an
561
 *   entity form.
562
563
 *
 * @todo Move to \Drupal\content_translation\ContentTranslationManager.
564
 */
565
function content_translation_form_controller(array $form_state) {
566
567
568
569
  return isset($form_state['controller']) && $form_state['controller'] instanceof EntityFormControllerInterface ? $form_state['controller'] : FALSE;
}

/**
570
 * Checks whether a content translation is accessible.
571
572
573
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity to be accessed.
574
575
576
577
578
579
 * @param $op
 *   The operation to be performed on the translation. Possible values are:
 *   - "view"
 *   - "update"
 *   - "delete"
 *   - "create"
580
581
582
 *
 * @return
 *   TRUE if the current user is allowed to view the translation.
583
584
 *
 * @todo Move to \Drupal\content_translation\ContentTranslationManager.
585
 */
586
587
function content_translation_access(EntityInterface $entity, $op) {
  return content_translation_controller($entity->entityType())->getTranslationAccess($entity, $op) ;
588
589
590
591
592
}

/**
 * Implements hook_permission().
 */
593
function content_translation_permission() {
594
  $permission = array(
595
    'administer content translation' => array(
596
      'title' => t('Administer translation settings'),
597
      'description' => t('Configure translatability of entities and fields.'),
598
    ),
599
    'create content translations' => array(
600
601
      'title' => t('Create translations'),
    ),
602
    'update content translations' => array(
603
604
      'title' => t('Edit translations'),
    ),
605
    'delete content translations' => array(
606
607
      'title' => t('Delete translations'),
    ),
608
609
610
611
612
    'translate any entity' => array(
      'title' => t('Translate any entity'),
    ),
  );

613
614
  // Create a translate permission for each enabled entity type and (optionally)
  // bundle.
615
616
617
  foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $info) {
    if ($permission_granularity = $info->getPermissionGranularity()) {
      $t_args = array('@entity_label' => $info->getLowercaseLabel());
618

619
      switch ($permission_granularity) {
620
621
        case 'bundle':
          foreach (entity_get_bundles($entity_type) as $bundle => $bundle_info) {
622
            if (content_translation_enabled($entity_type, $bundle)) {
623
              $t_args['%bundle_label'] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
624
              $permission["translate $bundle $entity_type"] = array(
625
                'title' => t('Translate %bundle_label @entity_label', $t_args),
626
627
628
629
630
631
              );
            }
          }
          break;

        case 'entity_type':
632
          if (content_translation_enabled($entity_type)) {
633
634
635
636
637
638
            $permission["translate $entity_type"] = array(
              'title' => t('Translate @entity_label', $t_args),
            );
          }
          break;
      }
639
640
641
642
643
644
645
646
647
    }
  }

  return $permission;
}

/**
 * Implements hook_form_alter().
 */
648
function content_translation_form_alter(array &$form, array &$form_state) {
649
650
651
652
  $form_controller = content_translation_form_controller($form_state);
  $entity = $form_controller ? $form_controller->getEntity() : NULL;

  if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1) {
653
    $controller = content_translation_controller($entity->entityType());
654
655
656
657
658
659
660
661
662
663
    $controller->entityFormAlter($form, $form_state, $entity);

    // @todo Move the following lines to the code generating the property form
    //   elements once we have an official #multilingual FAPI key.
    $translations = $entity->getTranslationLanguages();
    $form_langcode = $form_controller->getFormLangcode($form_state);

    // Handle fields shared between translations when there is at least one
    // translation available or a new one is being created.
    if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
664
665
      foreach ($entity->getPropertyDefinitions() as $property_name => $definition) {
        if (isset($form[$property_name])) {
666
          $form[$property_name]['#multilingual'] = $definition->isTranslatable();
667
668
669
670
671
672
673
        }
      }
    }

  }
}

674
/**
675
 * Implements hook_language_fallback_candidates_OPERATION_alter().
676
677
678
 *
 * Performs language fallback for unaccessible translations.
 */
679
680
681
682
683
function content_translation_language_fallback_candidates_entity_view_alter(&$candidates, $context) {
  $entity = $context['data'];
  foreach ($entity->getTranslationLanguages() as $langcode => $language) {
    if (!content_translation_view_access($entity, $langcode)) {
      unset($candidates[$langcode]);
684
685
686
687
    }
  }
}

688
689
690
/**
 * Implements hook_entity_load().
 */
691
function content_translation_entity_load(array $entities, $entity_type) {
692
693
  $enabled_entities = array();

694
  if (content_translation_enabled($entity_type)) {
695
    foreach ($entities as $entity) {
696
      if ($entity instanceof ContentEntityInterface && $entity->isTranslatable()) {
697
698
699
700
701
702
        $enabled_entities[$entity->id()] = $entity;
      }
    }
  }

  if (!empty($enabled_entities)) {
703
    content_translation_load_translation_metadata($enabled_entities, $entity_type);
704
705
706
707
708
709
710
711
712
713
714
  }
}

/**
 * Loads translation data into the given entities.
 *
 * @param array $entities
 *   The entities keyed by entity ID.
 * @param string $entity_type
 *   The type of the entities.
 */
715
716
function content_translation_load_translation_metadata(array $entities, $entity_type) {
  $query = 'SELECT * FROM {content_translation} te WHERE te.entity_type = :entity_type AND te.entity_id IN (:entity_id)';
717
  $result = db_query($query, array(':entity_type' => $entity_type, ':entity_id' => array_keys($entities)));
718
  $exclude = array('entity_type', 'entity_id', 'langcode');
719
720
721
  foreach ($result as $record) {
    $entity = $entities[$record->entity_id];
    // @todo Declare these as entity (translation?) properties.
722
723
    foreach ($record as $field_name => $value) {
      if (!in_array($field_name, $exclude)) {
724
725
726
727
728
        $langcode = $record->langcode;
        $entity->translation[$langcode][$field_name] = $value;
        if (!$entity->hasTranslation($langcode)) {
          $entity->initTranslation($langcode);
        }
729
730
      }
    }
731
732
733
734
735
736
  }
}

/**
 * Implements hook_entity_insert().
 */
737
function content_translation_entity_insert(EntityInterface $entity) {
738
  // Only do something if translation support for the given entity is enabled.
739
  if (!($entity instanceof ContentEntityInterface) || !$entity->isTranslatable()) {
740
741
742
    return;
  }

743
  $fields = array('entity_type', 'entity_id', 'langcode', 'source', 'outdated', 'uid', 'status', 'created', 'changed');
744
  $query = db_insert('content_translation')->fields($fields);
745
746

  foreach ($entity->getTranslationLanguages() as $langcode => $language) {
747
748
749
750
    $translation = isset($entity->translation[$langcode]) ? $entity->translation[$langcode] : array();

    $translation += array(
      'source' => '',
751
      'uid' => \Drupal::currentUser()->id(),
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
      'outdated' => FALSE,
      'status' => TRUE,
      'created' => REQUEST_TIME,
      'changed' => REQUEST_TIME,
    );

    $translation['entity_type'] = $entity->entityType();
    $translation['entity_id'] = $entity->id();
    $translation['langcode'] = $langcode;

    // Reorder values to match the schema.
    $values = array();
    foreach ($fields as $field_name) {
      $value = is_bool($translation[$field_name]) ? intval($translation[$field_name]) : $translation[$field_name];
      $values[$field_name] = $value;
    }
    $query->values($values);
769
770
771
772
773
774
775
776
  }

  $query->execute();
}

/**
 * Implements hook_entity_delete().
 */
777
function content_translation_entity_delete(EntityInterface $entity) {
778
  // Only do something if translation support for the given entity is enabled.
779
  if (!($entity instanceof ContentEntityInterface) || !$entity->isTranslatable()) {
780
781
782
    return;
  }

783
  db_delete('content_translation')
784
785
786
787
788
789
790
791
    ->condition('entity_type', $entity->entityType())
    ->condition('entity_id', $entity->id())
    ->execute();
}

/**
 * Implements hook_entity_update().
 */
792
function content_translation_entity_update(EntityInterface $entity) {
793
  // Only do something if translation support for the given entity is enabled.
794
  if (!($entity instanceof ContentEntityInterface) || !$entity->isTranslatable()) {
795
796
797
    return;
  }

798
  // Delete and create to ensure no stale value remains behind.
799
800
  content_translation_entity_delete($entity);
  content_translation_entity_insert($entity);
801
802
803
804
805
}

/**
 * Implements hook_field_extra_fields().
 */
806
function content_translation_field_extra_fields() {
807
808
  $extra = array();

809
  foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $info) {
810
    foreach (entity_get_bundles($entity_type) as $bundle => $bundle_info) {
811
      if (content_translation_enabled($entity_type, $bundle)) {
812
813
814
815
816
817
818
819
820
821
822
823
824
        $extra[$entity_type][$bundle]['form']['translation'] = array(
          'label' => t('Translation'),
          'description' => t('Translation settings'),
          'weight' => 10,
        );
      }
    }
  }

  return $extra;
}

/**
825
 * Implements hook_form_FORM_ID_alter() for 'field_ui_field_edit_form'.
826
 */
827
function content_translation_form_field_ui_field_edit_form_alter(array &$form, array &$form_state, $form_id) {
828
829
830
  $field = $form['#field'];
  $bundle = $form['#bundle'];
  $bundle_is_translatable = content_translation_enabled($field->entity_type, $bundle);
831
832
833
  $form['field']['translatable'] = array(
    '#type' => 'checkbox',
    '#title' => t('Users may translate this field.'),
834
    '#default_value' => $field->isTranslatable(),
835
    '#weight' => 20,
836
    '#disabled' => !$bundle_is_translatable,
837
  );
838
  $form['#submit'][] = 'content_translation_form_field_ui_field_edit_form_submit';
839
840
841
842
843
844
845
846
847
848

  // Provide helpful pointers for administrators.
  if (\Drupal::currentUser()->hasPermission('administer content translation') &&  !$bundle_is_translatable) {
    $toggle_url = url('admin/config/regional/content-language', array(
      'query' => drupal_get_destination(),
    ));
    $form['field']['translatable']['#description'] = t('To enable translation of this field, <a href="@language-settings-url">enable language support</a> for this type.', array(
      '@language-settings-url' => $toggle_url,
    ));
  }
849
850
851
852
853
854
855
856
857
858
859
}

/**
 * Form submission handler for 'field_ui_field_edit_form'.
 */
function content_translation_form_field_ui_field_edit_form_submit($form, array &$form_state) {
  $instance = $form_state['instance'];
  $value = content_translation_get_config($instance->entity_type, $instance->bundle, 'fields');
  if (!isset($value)) {
    $value = array();
  }
860
  $value[$instance->getField()->getName()] = $form_state['values']['field']['translatable'];
861
862
863
864
865
  // Store the same value for all bundles as translatability is tracked per
  // field.
  foreach (entity_get_bundles($instance->entity_type) as $bundle => $info) {
    content_translation_set_config($instance->entity_type, $bundle, 'fields', $value);
  }
866
867
}

868
/**
869
 * Implements hook_form_FORM_ID_alter() for 'field_ui_field_instance_edit_form'.
870
 */
871
function content_translation_form_field_ui_field_instance_edit_form_alter(array &$form, array &$form_state, $form_id) {
872
  if ($form['#field']->isTranslatable()) {
873
    module_load_include('inc', 'content_translation', 'content_translation.admin');
874
    $element = content_translation_field_sync_widget($form['#field']);
875
876
877
878
879
880
881
882
883
    if ($element) {
      $form['instance']['settings']['translation_sync'] = $element;
    }
  }
}

/**
 * Implements hook_field_info_alter().
 */
884
function content_translation_field_info_alter(&$info) {
885
  foreach ($info as &$field_type_info) {
886
887
888
889
890
891
892
893
    // By default no column has to be synchronized.
    $field_type_info['settings'] += array('translation_sync' => FALSE);
    // Synchronization can be enabled per instance.
    $field_type_info['instance_settings'] += array('translation_sync' => FALSE);
  }
}

/**
894
 * Implements hook_entity_presave().
895
 */
896
function content_translation_entity_presave(EntityInterface $entity) {
897
  if ($entity instanceof ContentEntityInterface && $entity->isTranslatable()) {
898
899
    // @todo Avoid using request attributes once translation metadata become
    //   regular fields.
900
901
    $attributes = \Drupal::request()->attributes;
    \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $entity->language()->id, $attributes->get('source_langcode'));
902
903
904
  }
}

905
906
907
/**
 * Implements hook_element_info_alter().
 */
908
function content_translation_element_info_alter(&$type) {
909
  if (isset($type['language_configuration'])) {
910
    $type['language_configuration']['#process'][] = 'content_translation_language_configuration_element_process';
911
912
  }
}
913

914
/**
915
 * Returns a widget to enable content translation per entity bundle.
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
 *
 * Backward compatibility layer to support entities not using the language
 * configuration form element.
 *
 * @todo Remove once all core entities have language configuration.
 *
 * @param string $entity_type
 *   The type of the entity being configured for translation.
 * @param string $bundle
 *   The bundle of the entity being configured for translation.
 * @param array $form
 *   The configuration form array.
 * @param array $form_state
 *   The configuration form state array.
 */
931
932
function content_translation_enable_widget($entity_type, $bundle, array &$form, array &$form_state) {
  $key = $form_state['content_translation']['key'];
933
934
935
936
  if (!isset($form_state['language'][$key])) {
    $form_state['language'][$key] = array();
  }
  $form_state['language'][$key] += array('entity_type' => $entity_type, 'bundle' => $bundle);
937
938
  $element = content_translation_language_configuration_element_process(array('#name' => $key), $form_state, $form);
  unset($element['content_translation']['#element_validate']);
939
940
941
942
943
944
945
946
947
948
949
950
  return $element;
}

/**
 * Process callback: Expands the language_configuration form element.
 *
 * @param array $element
 *   Form API element.
 *
 * @return
 *   Processed language configuration element.
 */
951
952
953
function content_translation_language_configuration_element_process(array $element, array &$form_state, array &$form) {
  if (empty($element['#content_translation_skip_alter']) && user_access('administer content translation')) {
    $form_state['content_translation']['key'] = $element['#name'];
954
    $context = $form_state['language'][$element['#name']];
955

956
    $element['content_translation'] = array(
957
958
      '#type' => 'checkbox',
      '#title' => t('Enable translation'),
959
960
      '#default_value' => content_translation_enabled($context['entity_type'], $context['bundle']),
      '#element_validate' => array('content_translation_language_configuration_element_validate'),
961
962
      '#prefix' => '<label>' . t('Translation') . '</label>',
    );
963

964
    $form['actions']['submit']['#submit'][] = 'content_translation_language_configuration_element_submit';
965
  }
966
967
968
969
  return $element;
}

/**
970
 * Form validation handler for element added with content_translation_language_configuration_element_process().
971
972
973
974
975
 *
 * Checks whether translation can be enabled: if language is set to one of the
 * special languages and language selector is not hidden, translation cannot be
 * enabled.
 *
976
 * @see content_translation_language_configuration_element_submit()
977
 */
978
979
function content_translation_language_configuration_element_validate($element, array &$form_state, array $form) {
  $key = $form_state['content_translation']['key'];
980
  $values = $form_state['values'][$key];
981
  if (!$values['language_show'] && $values['content_translation'] && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
982
    foreach (language_list(Language::STATE_LOCKED) as $language) {
983
984
985
986
987
      $locked_languages[] = $language->name;
    }
    // @todo Set the correct form element name as soon as the element parents
    //   are correctly set. We should be using NestedArray::getValue() but for
    //   now we cannot.
988
    form_set_error('', $form_state, t('"Show language selector" is not compatible with translating content that has default language: %choice. Either do not hide the language selector or pick a specific language.', array('%choice' => $locked_languages[$values['langcode']])));
989
990
991
992
  }
}

/**
993
 * Form submission handler for element added with content_translation_language_configuration_element_process().
994
 *
995
 * Stores the content translation settings.
996
 *
997
 * @see content_translation_language_configuration_element_validate()
998
 */
999
1000
function content_translation_language_configuration_element_submit(array $form, array &$form_state) {
  $key = $form_state['content_translation']['key'];
1001
  $context = $form_state['language'][$key];
1002
  $enabled = $form_state['values'][$key]['content_translation'];
1003

1004
1005
  if (content_translation_enabled($context['entity_type'], $context['bundle']) != $enabled) {
    content_translation_set_config($context['entity_type'], $context['bundle'], 'enabled', $enabled);
1006
1007
1008
1009
    entity_info_cache_clear();
    menu_router_rebuild();
  }
}
1010
1011
1012
1013

/**
 * Implements hook_form_FORM_ID_alter() for language_content_settings_form().
 */
1014
1015
1016
function content_translation_form_language_content_settings_form_alter(array &$form, array &$form_state) {
  module_load_include('inc', 'content_translation', 'content_translation.admin');
  _content_translation_form_language_content_settings_form_alter($form, $form_state);
1017
1018
}

1019
1020
1021
/**
 * Implements hook_preprocess_HOOK() for theme_language_content_settings_table().
 */
1022
1023
1024
function content_translation_preprocess_language_content_settings_table(&$variables) {
  module_load_include('inc', 'content_translation', 'content_translation.admin');
  _content_translation_preprocess_language_content_settings_table($variables);
1025
1026
}

1027
/**
1028
 * Stores content translation settings.
1029
1030
1031
1032
1033
1034
1035
1036
1037
 *
 * @param array $settings
 *   An associative array of settings keyed by entity type and bundle. At bundle
 *   level the following keys are available:
 *   - translatable: The bundle translatability status, which is a bool.
 *   - settings: An array of language configuration settings as defined by
 *     language_save_default_configuration().
 *   - fields: An associative array with field names as keys and a boolean as
 *     value, indicating field translatability.
1038
1039
 *   - columns: An associative array of translation synchronization settings
 *     keyed by field names.
1040
 */
1041
function content_translation_save_settings($settings) {
1042
1043
  foreach ($settings as $entity_type => $entity_settings) {
    foreach ($entity_settings as $bundle => $bundle_settings) {
1044
1045
1046
      // The 'translatable' value is set only if it is possible to enable.
      if (isset($bundle_settings['translatable'])) {
        // Store whether a bundle has translation enabled or not.
1047
        content_translation_set_config($entity_type, $bundle, 'enabled', $bundle_settings['translatable']);
1048

1049
1050
1051
1052
1053
        // Store whether fields are translatable or not.
        if (!empty($bundle_settings['fields'])) {
          content_translation_set_config($entity_type, $bundle, 'fields', $bundle_settings['fields']);
        }

1054
1055
1056
1057
        // Store whether fields have translation enabled or not.
        if (!empty($bundle_settings['columns'])) {
          foreach ($bundle_settings['columns'] as $field_name => $column_settings) {
            $instance = field_info_instance($entity_type, $field_name, $bundle);
1058
            if ($instance->isTranslatable()) {
1059
              $instance->settings['translation_sync'] = $column_settings;
1060
1061
1062
1063
            }
            // If the field does not have translatable enabled we need to reset
            // the sync settings to their defaults.
            else {
1064
              unset($instance->settings['translation_sync']);
1065
            }
1066
            $instance->save();
1067
1068
1069
          }
        }
      }
1070
1071
    }
  }
1072

1073
1074
1075
1076
  // Ensure entity and menu router information are correctly rebuilt.
  entity_info_cache_clear();
  menu_router_rebuild();
}