entity_translation.module 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119
  1. <?php
  2. /**
  3. * @file
  4. * Allows entities to be translated into different languages.
  5. */
  6. module_load_include('inc', 'entity_translation', 'entity_translation.node');
  7. module_load_include('inc', 'entity_translation', 'entity_translation.taxonomy');
  8. /**
  9. * Language code identifying the site default language.
  10. */
  11. define('ENTITY_TRANSLATION_LANGUAGE_DEFAULT', 'xx-et-default');
  12. /**
  13. * Language code identifying the current content language.
  14. */
  15. define('ENTITY_TRANSLATION_LANGUAGE_CURRENT', 'xx-et-current');
  16. /**
  17. * Language code identifying the author's preferred language.
  18. */
  19. define('ENTITY_TRANSLATION_LANGUAGE_AUTHOR', 'xx-et-author');
  20. /**
  21. * Defines an i18n translation mode for Entity Translation.
  22. */
  23. define('I18N_MODE_ENTITY_TRANSLATION', 32768);
  24. /**
  25. * Implements hook_hook_info().
  26. */
  27. function entity_translation_hook_info() {
  28. $hooks['entity_translation_insert'] = array(
  29. 'group' => 'entity_translation',
  30. );
  31. $hooks['entity_translation_update'] = array(
  32. 'group' => 'entity_translation',
  33. );
  34. $hooks['entity_translation_delete'] = array(
  35. 'group' => 'entity_translation',
  36. );
  37. return $hooks;
  38. }
  39. /**
  40. * Implements hook_module_implements_alter().
  41. */
  42. function entity_translation_module_implements_alter(&$implementations, $hook) {
  43. switch ($hook) {
  44. case 'menu_alter':
  45. case 'entity_info_alter':
  46. // Move some of our hook implementations to the end of the list.
  47. $group = $implementations['entity_translation'];
  48. unset($implementations['entity_translation']);
  49. $implementations['entity_translation'] = $group;
  50. break;
  51. }
  52. }
  53. /**
  54. * Implements hook_language_type_info_alter().
  55. */
  56. function entity_translation_language_types_info_alter(array &$language_types) {
  57. unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']);
  58. }
  59. /**
  60. * Implements hook_entity_info().
  61. */
  62. function entity_translation_entity_info() {
  63. $info = array();
  64. $info['node'] = array(
  65. 'translation' => array(
  66. 'entity_translation' => array(
  67. 'class' => 'EntityTranslationNodeHandler',
  68. 'access callback' => 'entity_translation_node_tab_access',
  69. 'access arguments' => array(1),
  70. 'admin theme' => variable_get('node_admin_theme'),
  71. 'bundle callback' => 'entity_translation_node_supported_type',
  72. 'default settings' => array(
  73. 'default_language' => LANGUAGE_NONE,
  74. 'hide_language_selector' => FALSE,
  75. ),
  76. ),
  77. ),
  78. );
  79. if (module_exists('comment')) {
  80. $info['comment'] = array(
  81. 'translation' => array(
  82. 'entity_translation' => array(
  83. 'class' => 'EntityTranslationCommentHandler',
  84. 'admin theme' => FALSE,
  85. 'bundle callback' => 'entity_translation_comment_supported_type',
  86. 'default settings' => array(
  87. 'default_language' => ENTITY_TRANSLATION_LANGUAGE_CURRENT,
  88. 'hide_language_selector' => TRUE,
  89. ),
  90. ),
  91. ),
  92. );
  93. }
  94. if (module_exists('taxonomy')) {
  95. $info['taxonomy_term'] = array(
  96. 'translation' => array(
  97. 'entity_translation' => array(
  98. 'class' => 'EntityTranslationTaxonomyTermHandler',
  99. 'access callback' => 'entity_translation_taxonomy_term_tab_access',
  100. 'access arguments' => array(1),
  101. 'base path' => 'taxonomy/term/%taxonomy_term',
  102. 'edit form' => 'term',
  103. 'bundle callback' => 'entity_translation_taxonomy_term_enabled_vocabulary',
  104. ),
  105. ),
  106. );
  107. }
  108. $info['user'] = array(
  109. 'translation' => array(
  110. 'entity_translation' => array(
  111. 'class' => 'EntityTranslationUserHandler',
  112. 'skip original values access' => TRUE,
  113. 'skip shared fields access' => TRUE,
  114. ),
  115. ),
  116. );
  117. return $info;
  118. }
  119. /**
  120. * Processes the given path schemes and fill-in default values.
  121. */
  122. function _entity_translation_process_path_schemes($entity_type, &$et_info) {
  123. $path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme', 'edit tabs'));
  124. // Insert the default path scheme into the 'path schemes' array and remove
  125. // respective elements from the entity_translation info array.
  126. $default_scheme = array_intersect_key($et_info, $path_scheme_keys);
  127. if (!empty($default_scheme)) {
  128. $et_info['path schemes']['default'] = $default_scheme;
  129. $et_info = array_diff_key($et_info, $path_scheme_keys);
  130. }
  131. // If no base path is provided we default to the common "node/%node"
  132. // pattern.
  133. if (empty($et_info['path schemes']['default']['base path'])) {
  134. $et_info['path schemes']['default']['base path'] = "$entity_type/%$entity_type";
  135. }
  136. foreach ($et_info['path schemes'] as $delta => $scheme) {
  137. // If there is a base path, then we automatically create the other path
  138. // elements based on the base path.
  139. if (!empty($scheme['base path'])) {
  140. $view_path = $scheme['base path'];
  141. $edit_path = $scheme['base path'] . '/edit';
  142. $translate_path = $scheme['base path'] . '/translate';
  143. $et_info['path schemes'][$delta] += array(
  144. 'view path' => $view_path,
  145. 'edit path' => $edit_path,
  146. 'translate path' => $translate_path,
  147. );
  148. }
  149. // Merge in default values for other scheme elements.
  150. $et_info['path schemes'][$delta] += array(
  151. 'admin theme' => TRUE,
  152. 'path wildcard' => "%$entity_type",
  153. 'edit tabs' => TRUE,
  154. );
  155. }
  156. }
  157. /**
  158. * Implements hook_entity_info_alter().
  159. */
  160. function entity_translation_entity_info_alter(&$entity_info) {
  161. // Provide defaults for translation info.
  162. foreach ($entity_info as $entity_type => $info) {
  163. if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) {
  164. $entity_info[$entity_type]['translation']['entity_translation'] = array();
  165. }
  166. $et_info = &$entity_info[$entity_type]['translation']['entity_translation'];
  167. // Every fieldable entity type must have a translation handler class and
  168. // translation keys defined, no matter if it is enabled for translation or
  169. // not. As a matter of fact we might need them to correctly switch field
  170. // translatability when a field is shared across different entity types.
  171. $et_info += array('class' => 'EntityTranslationDefaultHandler');
  172. if (!isset($entity_info[$entity_type]['entity keys'])) {
  173. $entity_info[$entity_type]['entity keys'] = array();
  174. }
  175. $entity_info[$entity_type]['entity keys'] += array('translations' => 'translations');
  176. if (entity_translation_enabled($entity_type, NULL, TRUE)) {
  177. $entity_info[$entity_type]['language callback'] = 'entity_translation_language';
  178. // Process path schemes and fill-in defaults.
  179. _entity_translation_process_path_schemes($entity_type, $et_info);
  180. // Merge in default values for remaining keys.
  181. $et_info += array(
  182. 'access callback' => 'entity_translation_tab_access',
  183. 'access arguments' => array($entity_type),
  184. );
  185. // Interpret a TRUE value for the 'edit form' key as the default value.
  186. if (!isset($et_info['edit form']) || $et_info['edit form'] === TRUE) {
  187. $et_info['edit form'] = $entity_type;
  188. }
  189. }
  190. }
  191. }
  192. /**
  193. * Implements hook_menu().
  194. */
  195. function entity_translation_menu() {
  196. $items = array();
  197. $items['admin/config/regional/entity_translation'] = array(
  198. 'title' => 'Entity translation',
  199. 'description' => 'Configure which entities can be translated and enable or disable language fallback.',
  200. 'page callback' => 'drupal_get_form',
  201. 'page arguments' => array('entity_translation_admin_form'),
  202. 'access arguments' => array('administer entity translation'),
  203. 'file' => 'entity_translation.admin.inc',
  204. 'module' => 'entity_translation',
  205. );
  206. $items['admin/config/regional/entity_translation/translatable/%'] = array(
  207. 'title' => 'Confirm change in translatability.',
  208. 'description' => 'Confirmation page for changing field translatability.',
  209. 'page callback' => 'drupal_get_form',
  210. 'page arguments' => array('entity_translation_translatable_form', 5),
  211. 'access arguments' => array('toggle field translatability'),
  212. 'file' => 'entity_translation.admin.inc',
  213. );
  214. $items['entity_translation/taxonomy_term/autocomplete'] = array(
  215. 'title' => 'Entity translation autocomplete',
  216. 'page callback' => 'entity_translation_taxonomy_term_autocomplete',
  217. 'access arguments' => array('access content'),
  218. 'type' => MENU_CALLBACK,
  219. );
  220. return $items;
  221. }
  222. /**
  223. * Validate the given set of path schemes and remove invalid elements.
  224. *
  225. * Each path scheme needs to fulfill the following requirements:
  226. * - The 'path wildcard' key needs to be specified.
  227. * - Every path (base/view/edit/translate) needs to contain the path wildcard.
  228. * - The following path definitions (if specified) need to match existing menu
  229. * items: 'base path', 'view path', 'edit path'.
  230. * - The 'translate path' definition needs to have an existing parent menu item.
  231. *
  232. * This function needs to be called once with a list of menu items passed as the
  233. * last parameter, before it can be used for validation.
  234. *
  235. * @param $schemes
  236. * The array of path schemes.
  237. * @param $entity_type_label
  238. * The label of the current entity type. This is used in error messages.
  239. * @param $items
  240. * A list of menu items.
  241. * @param $warnings
  242. * (optional) Displays warnings when a path scheme does not validate.
  243. */
  244. function _entity_translation_validate_path_schemes(&$schemes, $entity_type_label, $items = FALSE, $warnings = FALSE) {
  245. $paths = &drupal_static(__FUNCTION__);
  246. static $regex = '|%[^/]+|';
  247. if (!empty($items)) {
  248. // Some menu loaders in the item paths might have been altered: we need to
  249. // replace any menu loader with a plain % to check if base paths are still
  250. // compatible.
  251. $paths = array();
  252. foreach ($items as $path => $item) {
  253. $stripped_path = preg_replace($regex, '%', $path);
  254. $paths[$stripped_path] = $path;
  255. }
  256. }
  257. if (empty($schemes)) {
  258. return;
  259. }
  260. // Make sure we have a set of paths to validate the scheme against.
  261. if (empty($paths)) {
  262. // This should never happen.
  263. throw new Exception('The Entity Translation path scheme validation function has not been initialized properly.');
  264. }
  265. foreach ($schemes as $delta => &$scheme) {
  266. // Every path scheme needs to declare a path wildcard for the entity id.
  267. if (empty($scheme['path wildcard'])) {
  268. if ($warnings) {
  269. $t_args = array('%scheme' => $delta, '%entity_type' => $entity_type_label);
  270. watchdog('entity_translation', 'Entity Translation path scheme %scheme for entities of type %entity_type does not declare a path wildcard.', $t_args);
  271. }
  272. unset($schemes[$delta]);
  273. continue;
  274. }
  275. $wildcard = $scheme['path wildcard'];
  276. $validate_keys = array('base path' => FALSE, 'view path' => FALSE, 'edit path' => FALSE, 'translate path' => TRUE);
  277. foreach ($validate_keys as $key => $check_parent) {
  278. if (isset($scheme[$key])) {
  279. $path = $scheme[$key];
  280. $parts = explode('/', $path);
  281. $scheme[$key . ' parts'] = $parts;
  282. // Check that the path contains the path wildcard. Required for
  283. // determining the position of the entity id in the path (see
  284. // entity_translation_menu_alter()).
  285. if (!in_array($wildcard, $parts)) {
  286. if ($warnings) {
  287. $t_args = array('%path_key' => $key, '%entity_type' => $entity_type_label, '%wildcard' => $wildcard, '%path' => $path);
  288. drupal_set_message(t('Invalid %path_key defined for entities of type %entity_type: entity wildcard %wildcard not found in %path.', $t_args), 'warning');
  289. }
  290. unset($scheme[$key]);
  291. continue;
  292. }
  293. // Remove the trailing path element for paths requiring an existing
  294. // parent menu item (i.e. the "translate path").
  295. $trailing_path_element = FALSE;
  296. if ($check_parent) {
  297. $trailing_path_element = array_pop($parts);
  298. $path = implode('/', $parts);
  299. }
  300. $stripped_path = preg_replace($regex, '%', $path);
  301. if (!isset($paths[$stripped_path])) {
  302. if ($warnings) {
  303. $t_args = array('%path_key' => $key, '%entity_type' => $entity_type_label, '%path' => $path);
  304. $msg = $check_parent ?
  305. t('Invalid %path_key defined for entities of type %entity_type: parent menu item not found for %path', $t_args) :
  306. t('Invalid %path_key defined for entities of type %entity_type: matching menu item not found for %path', $t_args);
  307. drupal_set_message($msg, 'warning');
  308. }
  309. unset($scheme[$key]);
  310. }
  311. // If there is a matching menu item for the current scheme key, save
  312. // the real path, i.e. the path of the matching menu item.
  313. else {
  314. $real_path = $paths[$stripped_path];
  315. $real_parts = explode('/', $real_path);
  316. // Restore previously removed trailing path element.
  317. if ($trailing_path_element) {
  318. $real_path .= '/' . $trailing_path_element;
  319. $real_parts[] = $trailing_path_element;
  320. }
  321. $scheme['real ' . $key] = $real_path;
  322. $scheme['real ' . $key . ' parts'] = $real_parts;
  323. }
  324. }
  325. }
  326. }
  327. }
  328. /**
  329. * Implements hook_menu_alter().
  330. */
  331. function entity_translation_menu_alter(&$items) {
  332. $backup = array();
  333. // Initialize path schemes validation function with set of current menu items.
  334. $_null = NULL;
  335. _entity_translation_validate_path_schemes($_null, FALSE, $items);
  336. // Create tabs for all possible entity types.
  337. foreach (entity_get_info() as $entity_type => $info) {
  338. // Menu is rebuilt while determining entity translation base paths and
  339. // callbacks so we might not have them available yet.
  340. if (entity_translation_enabled($entity_type)) {
  341. $et_info = $info['translation']['entity_translation'];
  342. // Flag for tracking whether we have managed to attach the translate UI
  343. // successfully at least once.
  344. $translate_ui_attached = FALSE;
  345. // Validate path schemes for current entity type. Also removes invalid
  346. // ones and adds '... path parts' elements.
  347. _entity_translation_validate_path_schemes($et_info['path schemes'], $info['label'], FALSE, TRUE);
  348. foreach ($et_info['path schemes'] as $scheme) {
  349. $translate_item = NULL;
  350. $edit_item = NULL;
  351. // If we have a translate path then attach the translation UI, and
  352. // register the callback for deleting a translation.
  353. if (isset($scheme['translate path'])) {
  354. $translate_path = $scheme['translate path'];
  355. $keys = array('theme callback', 'theme arguments', 'access callback', 'access arguments', 'load arguments');
  356. $item = array_intersect_key($info['translation']['entity_translation'], drupal_map_assoc($keys));
  357. $item += array(
  358. 'file' => 'entity_translation.admin.inc',
  359. 'module' => 'entity_translation',
  360. );
  361. $entity_position = array_search($scheme['path wildcard'], $scheme['translate path parts']);
  362. if ($item['access callback'] == 'entity_translation_tab_access') {
  363. $item['access arguments'][] = $entity_position;
  364. }
  365. // Backup existing values for the translate overview page.
  366. if (isset($items[$translate_path])) {
  367. $backup[$entity_type] = $items[$translate_path];
  368. }
  369. $items[$translate_path] = array(
  370. 'title' => 'Translate',
  371. 'page callback' => 'entity_translation_overview',
  372. 'page arguments' => array($entity_type, $entity_position),
  373. 'type' => MENU_LOCAL_TASK,
  374. 'weight' => 2,
  375. 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  376. ) + $item;
  377. // Delete translation callback.
  378. $language_position = count($scheme['translate path parts']) + 1;
  379. $items["$translate_path/delete/%entity_translation_language"] = array(
  380. 'title' => 'Delete',
  381. 'page callback' => 'drupal_get_form',
  382. 'page arguments' => array('entity_translation_delete_confirm', $entity_type, $entity_position, $language_position),
  383. ) + $item;
  384. $translate_item = &$items[$translate_path];
  385. }
  386. // If we have an edit path, then replace the menu edit form with our
  387. // proxy implementation, and register new callbacks for adding and
  388. // editing a translation.
  389. if (isset($scheme['edit path'])) {
  390. // Find the edit item. If the edit path is a default local task we
  391. // need to find the parent item.
  392. $real_edit_path_parts = $scheme['real edit path parts'];
  393. do {
  394. $edit_item = &$items[implode('/', $real_edit_path_parts)];
  395. array_pop($real_edit_path_parts);
  396. }
  397. while (!empty($edit_item['type']) && $edit_item['type'] == MENU_DEFAULT_LOCAL_TASK);
  398. $edit_path = $scheme['edit path'];
  399. $edit_path_parts = $scheme['edit path parts'];
  400. // Replace the main edit callback with our proxy implementation to set
  401. // form language to the current language and check access.
  402. $entity_position = array_search($scheme['path wildcard'], $edit_path_parts);
  403. // Make sure incoming page and access arguments are arrays.
  404. $original_item = $edit_item + array(
  405. 'page arguments' => array(),
  406. 'access arguments' => array(),
  407. );
  408. $args = array($entity_type, $entity_position, FALSE, $original_item);
  409. $edit_item['page callback'] = 'entity_translation_edit_page';
  410. $edit_item['page arguments'] = array_merge($args, $original_item['page arguments']);
  411. $edit_item['access callback'] = 'entity_translation_edit_access';
  412. $edit_item['access arguments'] = array_merge($args, $original_item['access arguments']);
  413. // Edit translation callback.
  414. if ($scheme['edit tabs'] !== FALSE) {
  415. $translation_position = count($edit_path_parts);
  416. $args = array($entity_type, $entity_position, $translation_position, $original_item);
  417. $items["$edit_path/%entity_translation_language"] = array(
  418. 'type' => MENU_DEFAULT_LOCAL_TASK,
  419. 'title callback' => 'entity_translation_edit_title',
  420. 'title arguments' => array($translation_position),
  421. 'page callback' => 'entity_translation_edit_page',
  422. 'page arguments' => array_merge($args, $original_item['page arguments']),
  423. 'access callback' => 'entity_translation_edit_access',
  424. 'access arguments' => array_merge($args, $original_item['access arguments']),
  425. )
  426. // We need to inherit the remaining menu item keys, mostly 'module'
  427. // and 'file' to keep ajax callbacks working (see form_get_cache() and
  428. // drupal_retrieve_form()).
  429. + $original_item;
  430. }
  431. // Add translation callback.
  432. $add_path = "$edit_path/add/%entity_translation_language/%entity_translation_language";
  433. $source_position = count($edit_path_parts) + 1;
  434. $target_position = count($edit_path_parts) + 2;
  435. $args = array($entity_type, $entity_position, $source_position, $target_position, $original_item);
  436. $items[$add_path] = array(
  437. 'title callback' => 'Add translation',
  438. 'page callback' => 'entity_translation_add_page',
  439. 'page arguments' => array_merge($args, $original_item['page arguments']),
  440. 'type' => MENU_LOCAL_TASK,
  441. 'access callback' => 'entity_translation_add_access',
  442. 'access arguments' => array_merge($args, $original_item['access arguments']),
  443. ) + $original_item;
  444. }
  445. // Make the "Translate" tab follow the "Edit" tab if possible.
  446. if ($translate_item && $edit_item && isset($edit_item['weight'])) {
  447. $translate_item['weight'] = $edit_item['weight'] + 1;
  448. }
  449. // If we have both an edit item and a translate item, then we know that
  450. // the translate UI has been attached properly (at least once).
  451. $translate_ui_attached = $translate_ui_attached || ($translate_item && $edit_item);
  452. // Cleanup reference variables, so we don't accidentially overwrite
  453. // something in a later iteration.
  454. unset($translate_item, $edit_item);
  455. }
  456. if ($translate_ui_attached == FALSE) {
  457. watchdog('entity_translation', 'The entities of type %entity_type do not define a valid path scheme: it will not be possible to translate them.', array('%entity_type' => $info['label']));
  458. }
  459. // Entity-type-specific menu alterations.
  460. $function = 'entity_translation_' . $entity_type . '_menu_alter';
  461. if (function_exists($function)) {
  462. $function($items, $backup);
  463. }
  464. }
  465. }
  466. // Avoid bloating memory with unused data.
  467. drupal_static_reset('_entity_translation_validate_path_schemes');
  468. }
  469. /**
  470. * Title callback.
  471. */
  472. function entity_translation_edit_title($langcode) {
  473. $languages = entity_translation_languages();
  474. return isset($languages[$langcode]) ? t($languages[$langcode]->name) : '';
  475. }
  476. /**
  477. * Page callback.
  478. */
  479. function entity_translation_edit_page() {
  480. $args = func_get_args();
  481. $entity_type = array_shift($args);
  482. $entity = array_shift($args);
  483. $langcode = array_shift($args);
  484. $edit_form_item = array_shift($args);
  485. // Set the current form language.
  486. $handler = entity_translation_get_handler($entity_type, $entity);
  487. $handler->initPathScheme();
  488. $langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
  489. $handler->setActiveLanguage($langcode);
  490. // Display the entity edit form.
  491. return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item);
  492. }
  493. /**
  494. * Access callback.
  495. */
  496. function entity_translation_edit_access() {
  497. $args = func_get_args();
  498. $entity_type = array_shift($args);
  499. $entity = array_shift($args);
  500. $langcode = array_shift($args);
  501. $edit_form_item = array_shift($args);
  502. $access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
  503. $handler = entity_translation_get_handler($entity_type, $entity);
  504. // First, check a handler has been loaded. This could be empty if a
  505. // non-existent entity edit path has been requested, for example. Delegate
  506. // directly to the edit form item access callback in this case.
  507. if (empty($handler)) {
  508. return _entity_translation_callback($access_callback, $args, $edit_form_item);
  509. }
  510. $translations = $handler->getTranslations();
  511. $langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
  512. // The user must be explicitly allowed to access the original values if
  513. // workflow permissions are enabled.
  514. if (!$handler->getTranslationAccess($langcode)) {
  515. return FALSE;
  516. }
  517. // If the translation exists or no translation was specified, we can show the
  518. // corresponding local task. If translations have not been initialized yet, we
  519. // need to grant access to the user.
  520. if (empty($translations->data) || isset($translations->data[$langcode])) {
  521. // Check that the requested language is actually accessible. If the entity
  522. // is language neutral we need to let editors access it.
  523. $enabled_languages = entity_translation_languages($entity_type, $entity);
  524. if (isset($enabled_languages[$langcode]) || $langcode == LANGUAGE_NONE) {
  525. return _entity_translation_callback($access_callback, $args, $edit_form_item);
  526. }
  527. }
  528. return FALSE;
  529. }
  530. /**
  531. * Determines the current form language.
  532. *
  533. * @param $langcode
  534. * The requested language code.
  535. * @param EntityTranslationHandlerInterface $handler
  536. * A translation handler instance.
  537. *
  538. * @return
  539. * A valid language code.
  540. *
  541. * @deprecated This is no longer used and will be removed in the first stable
  542. * release.
  543. */
  544. function entity_translation_form_language($langcode, $handler) {
  545. return entity_translation_get_existing_language($handler->getEntity(), $handler->getEntityType(), $langcode);
  546. }
  547. /**
  548. * Determines an existing translation language.
  549. *
  550. * Based on the requested language and the translations available for the given
  551. * entity, determines an existing translation language. This takes into account
  552. * language fallback rules.
  553. *
  554. * @param $entity_type
  555. * The type of the entity.
  556. * @param $entity
  557. * The entity whose existing translation language has to be returned.
  558. * @param $langcode
  559. * (optional) The requested language code. Defaults to the current content
  560. * language.
  561. *
  562. * @return
  563. * A valid language code.
  564. */
  565. function entity_translation_get_existing_language($entity_type, $entity, $langcode = NULL) {
  566. $handler = entity_translation_get_handler($entity_type, $entity);
  567. if (empty($langcode)) {
  568. $langcode = $GLOBALS['language_content']->language;
  569. }
  570. $translations = $handler->getTranslations();
  571. $fallback = drupal_multilingual() ? language_fallback_get_candidates() : array(LANGUAGE_NONE);
  572. while (!empty($langcode) && !isset($translations->data[$langcode])) {
  573. $langcode = array_shift($fallback);
  574. }
  575. // If no translation is available fall back to the entity language.
  576. return !empty($langcode) ? $langcode : $handler->getLanguage();
  577. }
  578. /**
  579. * Access callback.
  580. */
  581. function entity_translation_add_access() {
  582. $args = func_get_args();
  583. $entity_type = array_shift($args);
  584. $entity = array_shift($args);
  585. $source = array_shift($args);
  586. $langcode = array_shift($args);
  587. $handler = entity_translation_get_handler($entity_type, $entity);
  588. $translations = $handler->getTranslations();
  589. // If the translation does not exist we can show the tab.
  590. if (!isset($translations->data[$langcode]) && $langcode != $source) {
  591. // Check that the requested language is actually accessible.
  592. $enabled_languages = entity_translation_languages($entity_type, $entity);
  593. if (isset($enabled_languages[$langcode])) {
  594. $edit_form_item = array_shift($args);
  595. $access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
  596. return _entity_translation_callback($access_callback, $args, $edit_form_item);
  597. }
  598. }
  599. return FALSE;
  600. }
  601. /**
  602. * Page callback.
  603. */
  604. function entity_translation_add_page() {
  605. $args = func_get_args();
  606. $entity_type = array_shift($args);
  607. $entity = array_shift($args);
  608. $source = array_shift($args);
  609. $langcode = array_shift($args);
  610. $edit_form_item = array_shift($args);
  611. $handler = entity_translation_get_handler($entity_type, $entity);
  612. $handler->initPathScheme();
  613. $handler->setActiveLanguage($langcode);
  614. $handler->setSourceLanguage($source);
  615. // Display the entity edit form.
  616. return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item);
  617. }
  618. /**
  619. * Helper function. Proxies a callback call including any needed file.
  620. */
  621. function _entity_translation_callback($callback, $args, $info = array()) {
  622. if (isset($info['file'])) {
  623. $path = isset($info['file path']) ? $info['file path'] : drupal_get_path('module', $info['module']);
  624. include_once DRUPAL_ROOT . '/' . $path . '/' . $info['file'];
  625. }
  626. return call_user_func_array($callback, $args);
  627. }
  628. /**
  629. * Implements hook_admin_paths().
  630. */
  631. function entity_translation_admin_paths() {
  632. $paths = array();
  633. foreach (entity_get_info() as $entity_type => $info) {
  634. if (isset($info['translation']['entity_translation']['path schemes']) && entity_translation_enabled($entity_type, NULL, TRUE)) {
  635. foreach ($info['translation']['entity_translation']['path schemes'] as $scheme) {
  636. if (!empty($scheme['admin theme'])) {
  637. if (isset($scheme['translate path'])) {
  638. $translate_path = preg_replace('|%[^/]*|', '*', $scheme['translate path']);
  639. $paths[$translate_path] = TRUE;
  640. $paths["$translate_path/*"] = TRUE;
  641. }
  642. if (isset($scheme['edit path'])) {
  643. $edit_path = preg_replace('|%[^/]*|', '*', $scheme['edit path']);
  644. $paths["$edit_path/*"] = TRUE;
  645. }
  646. }
  647. }
  648. }
  649. }
  650. return $paths;
  651. }
  652. /**
  653. * Access callback.
  654. */
  655. function entity_translation_tab_access($entity_type, $entity) {
  656. if (drupal_multilingual() && (user_access('translate any entity') || user_access("translate $entity_type entities"))) {
  657. $handler = entity_translation_get_handler($entity_type, $entity);
  658. // Ensure $entity holds an entity object and not an id.
  659. $entity = $handler->getEntity();
  660. $enabled = entity_translation_enabled($entity_type, $entity);
  661. return $enabled && $handler->getLanguage() != LANGUAGE_NONE;
  662. }
  663. return FALSE;
  664. }
  665. /**
  666. * Menu loader callback.
  667. */
  668. function entity_translation_language_load($langcode, $entity_type = NULL, $entity = NULL) {
  669. $enabled_languages = entity_translation_languages($entity_type, $entity);
  670. return isset($enabled_languages[$langcode]) ? $langcode : FALSE;
  671. }
  672. /**
  673. * Menu loader callback.
  674. */
  675. function entity_translation_menu_entity_load($entity_id, $entity_type) {
  676. $entities = entity_load($entity_type, array($entity_id));
  677. return $entities[$entity_id];
  678. }
  679. /**
  680. * Implements hook_permission().
  681. */
  682. function entity_translation_permission() {
  683. $permission = array(
  684. 'administer entity translation' => array(
  685. 'title' => t('Administer entity translation'),
  686. 'description' => t('Select which entities can be translated.'),
  687. ),
  688. 'toggle field translatability' => array(
  689. 'title' => t('Toggle field translatability'),
  690. 'description' => t('Toggle translatability of fields performing a bulk update.'),
  691. ),
  692. 'translate any entity' => array(
  693. 'title' => t('Translate any entity'),
  694. 'description' => t('Translate field content for any fieldable entity.'),
  695. ),
  696. );
  697. $workflow = entity_translation_workflow_enabled();
  698. if ($workflow) {
  699. $permission += array(
  700. 'edit translation shared fields' => array(
  701. 'title' => t('Edit shared values'),
  702. 'description' => t('Edit values shared between translations on the entity form.'),
  703. ),
  704. 'edit original values' => array(
  705. 'title' => t('Edit original values'),
  706. 'description' => t('Access any entity form in the original language.'),
  707. ),
  708. );
  709. }
  710. foreach (entity_get_info() as $entity_type => $info) {
  711. if ($info['fieldable'] && entity_translation_enabled($entity_type)) {
  712. $label = !empty($info['label']) ? t($info['label']) : $entity_type;
  713. $permission["translate $entity_type entities"] = array(
  714. 'title' => t('Translate entities of type @type', array('@type' => $label)),
  715. 'description' => t('Translate field content for entities of type @type.', array('@type' => $label)),
  716. );
  717. if ($workflow) {
  718. // Avoid access control for original values on the current entity.
  719. if (empty($info['translation']['entity_translation']['skip original values access'])) {
  720. $permission["edit $entity_type original values"] = array(
  721. 'title' => t('Edit original values on entities of type @type', array('@type' => $label)),
  722. 'description' => t('Access the entity form in the original language for entities of type @type.', array('@type' => $label)),
  723. );
  724. }
  725. // Avoid access control for shared fields on the current entity.
  726. if (empty($info['translation']['entity_translation']['skip shared fields access'])) {
  727. $permission["edit $entity_type translation shared fields"] = array(
  728. 'title' => t('Edit @type shared values.', array('@type' => $label)),
  729. 'description' => t('Edit values shared between translations on @type forms.', array('@type' => $label)),
  730. );
  731. }
  732. }
  733. }
  734. }
  735. return $permission;
  736. }
  737. /**
  738. * Returns TRUE if the translation workflow is enabled.
  739. */
  740. function entity_translation_workflow_enabled() {
  741. return variable_get('entity_translation_workflow_enabled', FALSE);
  742. }
  743. /**
  744. * Implements hook_theme().
  745. */
  746. function entity_translation_theme() {
  747. return array(
  748. 'entity_translation_unavailable' => array(
  749. 'variables' => array('element' => NULL),
  750. ),
  751. 'entity_translation_language_tabs' => array(
  752. 'render element' => 'element',
  753. ),
  754. 'entity_translation_overview' => array(
  755. 'variables' => array('rows' => NULL, 'header' => NULL),
  756. 'file' => 'entity_translation.admin.inc'
  757. ),
  758. 'entity_translation_overview_outdated' => array(
  759. 'variables' => array('message' => NULL),
  760. 'file' => 'entity_translation.admin.inc'
  761. ),
  762. );
  763. }
  764. /**
  765. * Implements hook_entity_load().
  766. */
  767. function entity_translation_entity_load($entities, $entity_type) {
  768. if (entity_translation_enabled($entity_type)) {
  769. EntityTranslationDefaultHandler::loadMultiple($entity_type, $entities);
  770. }
  771. }
  772. /**
  773. * Implements hook_field_extra_fields().
  774. */
  775. function entity_translation_field_extra_fields() {
  776. $extra = array();
  777. $enabled = variable_get('entity_translation_entity_types', array());
  778. $info = entity_get_info();
  779. foreach ($enabled as $entity_type) {
  780. if (entity_translation_enabled($entity_type)) {
  781. $bundles = !empty($info[$entity_type]['bundles']) ? array_keys($info[$entity_type]['bundles']) : array($entity_type);
  782. foreach ($bundles as $bundle) {
  783. $settings = entity_translation_settings($entity_type, $bundle);
  784. if (empty($settings['hide_language_selector']) && entity_translation_enabled_bundle($entity_type, $bundle) && ($handler = entity_translation_get_handler($entity_type, $bundle))) {
  785. $language_key = $handler->getLanguageKey();
  786. $extra[$entity_type][$bundle] = array(
  787. 'form' => array(
  788. $language_key => array(
  789. 'label' => t('Language'),
  790. 'description' => t('Language selection'),
  791. 'weight' => 5,
  792. ),
  793. ),
  794. );
  795. }
  796. }
  797. }
  798. }
  799. return $extra;
  800. }
  801. /**
  802. * Implements hook_field_language_alter().
  803. *
  804. * Performs language fallback for unaccessible translations.
  805. */
  806. function entity_translation_field_language_alter(&$display_language, $context) {
  807. if (variable_get('locale_field_language_fallback', TRUE) && entity_translation_enabled($context['entity_type'])) {
  808. $entity = $context['entity'];
  809. $entity_type = $context['entity_type'];
  810. $handler = entity_translation_get_handler($entity_type, $entity);
  811. $translations = $handler->getTranslations();
  812. // Apply fallback only on unpublished translations as missing translations
  813. // are already handled in locale_field_language_alter().
  814. if (isset($translations->data[$context['language']]) && !entity_translation_access($entity_type, $translations->data[$context['language']])) {
  815. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  816. $instances = field_info_instances($entity_type, $bundle);
  817. $entity = clone $entity;
  818. foreach ($translations->data as $langcode => $translation) {
  819. if ($langcode == $context['language'] || !entity_translation_access($entity_type, $translations->data[$langcode])) {
  820. // Unset unaccessible field translations: if the field is
  821. // untranslatable unsetting a language different from LANGUAGE_NONE
  822. // has no effect.
  823. foreach ($instances as $instance) {
  824. unset($entity->{$instance['field_name']}[$langcode]);
  825. }
  826. }
  827. }
  828. // Find the new fallback values.
  829. locale_field_language_fallback($display_language, $entity, $context['language']);
  830. }
  831. elseif (!field_has_translation_handler($entity_type, 'locale')) {
  832. // If not handled by the Locale translation handler trigger fallback too.
  833. locale_field_language_fallback($display_language, $entity, $context['language']);
  834. }
  835. }
  836. }
  837. /**
  838. * Implements hook_field_attach_view_alter().
  839. *
  840. * Hide the entity if no translation is available for the current language and
  841. * language fallback is disabled.
  842. */
  843. function entity_translation_field_attach_view_alter(&$output, $context) {
  844. if (!variable_get('locale_field_language_fallback', TRUE) && entity_translation_enabled($context['entity_type'])) {
  845. $handler = entity_translation_get_handler($context['entity_type'], $context['entity']);
  846. $translations = $handler->getTranslations();
  847. $langcode = !empty($context['language']) ? $context['language'] : $GLOBALS['language_content']->language;
  848. // If fallback is disabled we need to notify the user that the translation
  849. // is unavailable (missing or unpublished).
  850. if (!empty($translations->data) && ((!isset($translations->data[$langcode]) && !isset($translations->data[LANGUAGE_NONE])) || ((isset($translations->data[$langcode]) && !entity_translation_access($context['entity_type'], $translations->data[$langcode]))))) {
  851. // Provide context for rendering.
  852. $output['#entity'] = $context['entity'];
  853. $output['#entity_type'] = $context['entity_type'];
  854. $output['#view_mode'] = $context['view_mode'];
  855. // We perform theming here because the theming function might need to set
  856. // system messages. It would be too late in the #post_render callback.
  857. $output['#entity_translation_unavailable'] = theme('entity_translation_unavailable', array('element' => $output));
  858. // As we used a string key, other modules implementing
  859. // hook_field_attach_view_alter() may unset/override this.
  860. $output['#post_render']['entity_translation'] = 'entity_translation_unavailable';
  861. }
  862. }
  863. }
  864. /**
  865. * Override the entity output with the unavailable translation one.
  866. */
  867. function entity_translation_unavailable($children, $element) {
  868. return $element['#entity_translation_unavailable'];
  869. }
  870. /**
  871. * Theme an unvailable translation.
  872. */
  873. function theme_entity_translation_unavailable($variables) {
  874. $element = $variables['element'];
  875. $handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
  876. $args = array('%language' => t($GLOBALS['language_content']->name), '%label' => $handler->getLabel());
  877. $message = t('%language translation unavailable for %label.', $args);
  878. $classes = $element['#entity_type'] . ' ' . $element['#entity_type'] . '-' . $element['#view_mode'];
  879. return "<div class=\"$classes\"><div class=\"messages warning\">$message</div></div>";
  880. }
  881. /**
  882. * Implements hook_field_info_alter().
  883. */
  884. function entity_translation_field_info_alter(&$info) {
  885. $columns = array('fid');
  886. $supported_types = array('file' => $columns, 'image' => $columns);
  887. foreach ($info as $field_type => &$field_type_info) {
  888. // Store columns to be synchronized.
  889. if (!isset($field_type_info['settings'])) {
  890. $field_type_info['settings'] = array();
  891. }
  892. $field_type_info['settings'] += array(
  893. 'entity_translation_sync' => isset($supported_types[$field_type]) ? $supported_types[$field_type] : FALSE,
  894. );
  895. // Synchronization can be enabled per instance.
  896. if (!isset($field_type_info['instance_settings'])) {
  897. $field_type_info['instance_settings'] = array();
  898. }
  899. $field_type_info['instance_settings'] += array(
  900. 'entity_translation_sync' => FALSE,
  901. );
  902. }
  903. }
  904. /**
  905. * Implements hook_field_attach_presave().
  906. */
  907. function entity_translation_field_attach_presave($entity_type, $entity) {
  908. if (entity_translation_enabled($entity_type, $entity)) {
  909. entity_translation_sync($entity_type, $entity);
  910. }
  911. }
  912. /**
  913. * Performs field column synchronization.
  914. */
  915. function entity_translation_sync($entity_type, $entity) {
  916. // If we are creating a new entity or if we have no translations for the
  917. // current entity, there is nothing to synchronize.
  918. $handler = entity_translation_get_handler($entity_type, $entity, TRUE);
  919. $translations = $handler->getTranslations();
  920. $original_langcode = $handler->getSourceLanguage();
  921. if ($handler->isNewEntity() || (count($translations->data) < 2 && !$original_langcode)) {
  922. return;
  923. }
  924. list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
  925. $instances = field_info_instances($entity_type, $bundle);
  926. $entity_unchanged = isset($entity->original) ? $entity->original : entity_load_unchanged($entity_type, $id);
  927. // If the entity language is being changed there is nothing to synchronize.
  928. $langcode = $handler->getLanguage();
  929. $handler->setEntity($entity_unchanged);
  930. if ($langcode != $handler->getLanguage()) {
  931. return;
  932. }
  933. foreach ($instances as $field_name => $instance) {
  934. $field = field_info_field($field_name);
  935. // If the field is empty there is nothing to synchronize. Synchronization
  936. // makes sense only for translatable fields.
  937. if (!empty($entity->{$field_name}) && !empty($instance['settings']['entity_translation_sync']) && field_is_translatable($entity_type, $field)) {
  938. $columns = $field['settings']['entity_translation_sync'];
  939. $change_map = array();
  940. $source_langcode = entity_language($entity_type, $entity);
  941. $source_items = $entity->{$field_name}[$source_langcode];
  942. // If a translation is being created, the original values should be used
  943. // as the unchanged items. In fact there are no unchanged items to check
  944. // against.
  945. $langcode = $original_langcode ? $original_langcode : $source_langcode;
  946. $unchanged_items = !empty($entity_unchanged->{$field_name}[$langcode]) ? $entity_unchanged->{$field_name}[$langcode] : array();
  947. // By picking the maximum size between updated and unchanged items, we
  948. // make sure to process also removed items.
  949. $total = max(array(count($source_items), count($unchanged_items)));
  950. // Make sure we can detect any change in the source items.
  951. for ($delta = 0; $delta < $total; $delta++) {
  952. foreach ($columns as $column) {
  953. // Store the delta for the unchanged column value.
  954. if (isset($unchanged_items[$delta][$column])) {
  955. $value = $unchanged_items[$delta][$column];
  956. $change_map[$column][$value]['old'] = $delta;
  957. }
  958. // Store the delta for the new column value.
  959. if (isset($source_items[$delta][$column])) {
  960. $value = $source_items[$delta][$column];
  961. $change_map[$column][$value]['new'] = $delta;
  962. }
  963. }
  964. }
  965. // Backup field values.
  966. $field_values = $entity->{$field_name};
  967. // Reset field values so that no spurious translation value is stored.
  968. // Source values and anything else must be preserved in any case.
  969. $entity->{$field_name} = array($source_langcode => $source_items) + array_diff_key($entity->{$field_name}, $translations->data);
  970. // Update translations.
  971. foreach ($translations->data as $langcode => $translation) {
  972. // We need to synchronize only values different from the source ones.
  973. if ($langcode != $source_langcode) {
  974. // Process even removed items.
  975. for ($delta = 0; $delta < $total; $delta++) {
  976. $created = TRUE;
  977. $removed = TRUE;
  978. foreach ($columns as $column) {
  979. if (isset($source_items[$delta][$column])) {
  980. $value = $source_items[$delta][$column];
  981. $created = $created && !isset($change_map[$column][$value]['old']);
  982. $removed = $removed && !isset($change_map[$column][$value]['new']);
  983. }
  984. }
  985. // If an item has been removed we do not store its translations.
  986. if ($removed) {
  987. // Ensure items are actually removed.
  988. if (!isset($entity->{$field_name}[$langcode])) {
  989. $entity->{$field_name}[$langcode] = array();
  990. }
  991. continue;
  992. }
  993. // If a synchronized column has changed we need to override the full
  994. // items array for all languages.
  995. elseif ($created) {
  996. $entity->{$field_name}[$langcode][$delta] = $source_items[$delta];
  997. }
  998. // The current item might have been reordered.
  999. elseif (!empty($change_map[$column][$value])) {
  1000. $old_delta = $change_map[$column][$value]['old'];
  1001. $new_delta = $change_map[$column][$value]['new'];
  1002. // If for nay reason the old value is not defined for the current
  1003. // we language we fall back to the new source value.
  1004. $items = isset($field_values[$langcode][$old_delta]) ? $field_values[$langcode][$old_delta] : $source_items[$new_delta];
  1005. $entity->{$field_name}[$langcode][$new_delta] = $items;
  1006. }
  1007. }
  1008. }
  1009. }
  1010. }
  1011. }
  1012. }
  1013. /**
  1014. * Implements hook_field_attach_insert().
  1015. */
  1016. function entity_translation_field_attach_insert($entity_type, $entity) {
  1017. // Store entity translation metadata only if the entity bundle is
  1018. // translatable.
  1019. if (entity_translation_enabled($entity_type, $entity)) {
  1020. $handler = entity_translation_get_handler($entity_type, $entity);
  1021. $handler->initTranslations();
  1022. $handler->saveTranslations();
  1023. }
  1024. }
  1025. /**
  1026. * Implements hook_field_attach_update().
  1027. */
  1028. function entity_translation_field_attach_update($entity_type, $entity) {
  1029. // Store entity translation metadata only if the entity bundle is
  1030. // translatable.
  1031. if (entity_translation_enabled($entity_type, $entity)) {
  1032. $handler = entity_translation_get_handler($entity_type, $entity, TRUE);
  1033. $handler->updateTranslations();
  1034. $handler->saveTranslations();
  1035. }
  1036. }
  1037. /**
  1038. * Implements hook_field_attach_delete().
  1039. */
  1040. function entity_translation_field_attach_delete($entity_type, $entity) {
  1041. if (entity_translation_enabled($entity_type, $entity)) {
  1042. $handler = entity_translation_get_handler($entity_type, $entity);
  1043. $handler->removeTranslations();
  1044. $handler->saveTranslations();
  1045. }
  1046. }
  1047. /**
  1048. * Implements hook_field_attach_delete_revision().
  1049. */
  1050. function entity_translation_field_attach_delete_revision($entity_type, $entity) {
  1051. if (entity_translation_enabled($entity_type, $entity)) {
  1052. $handler = entity_translation_get_handler($entity_type, $entity);
  1053. $handler->removeRevisionTranslations();
  1054. $handler->saveTranslations();
  1055. }
  1056. }
  1057. /**
  1058. * Implementation of hook_field_attach_form().
  1059. */
  1060. function entity_translation_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
  1061. // Avoid recursing into the source form.
  1062. list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
  1063. if (empty($form['#entity_translation_source_form']) && entity_translation_enabled($entity_type, $bundle)) {
  1064. $handler = entity_translation_get_handler($entity_type, $entity);
  1065. $langcode = !empty($langcode) ? $langcode : $handler->getLanguage();
  1066. $form_langcode = $handler->getActiveLanguage();
  1067. $translations = $handler->getTranslations();
  1068. $update_langcode = $form_langcode && ($form_langcode != $langcode);
  1069. $source = $handler->getSourceLanguage();
  1070. $new_translation = !isset($translations->data[$form_langcode]);
  1071. // If we are creating a new translation we need to retrieve form elements
  1072. // populated with the source language values, but only if form is not being
  1073. // rebuilt. In this case source values have already been populated, so we
  1074. // need to preserve possible changes. There might be situations, e.g. ajax
  1075. // calls, where the form language has not been properly initialized before
  1076. // calling field_attach_form(). In this case we need to rebuild the form
  1077. // with the correct form language and replace the field elements with the
  1078. // correct ones.
  1079. if ($update_langcode || ($source && !isset($translations->data[$form_langcode]) && isset($translations->data[$source]) && empty($form_state['rebuild']))) {
  1080. foreach (field_info_instances($entity_type, $bundle) as $instance) {
  1081. $field_name = $instance['field_name'];
  1082. $field = field_info_field($field_name);
  1083. // If we are creating a new translation we have to change the form item
  1084. // language information from source to target language, this way the
  1085. // user can find the form items already populated with the source values
  1086. // while the field form element holds the correct language information.
  1087. if ($field['translatable'] && isset($form[$field_name])) {
  1088. $element = &$form[$field_name];
  1089. $element['#entity_type'] = $entity_type;
  1090. $element['#entity'] = $entity;
  1091. $element['#entity_id'] = $id;
  1092. $element['#field_name'] = $field_name;
  1093. $element['#source'] = $update_langcode ? $form_langcode : $source;
  1094. $element['#previous'] = NULL;
  1095. $element['#form_parents'] = $form['#parents'];
  1096. // If we are updating the form language we need to make sure that the
  1097. // wrong language is unset and the right one is stored in the field
  1098. // element (see entity_translation_prepare_element()).
  1099. if ($update_langcode) {
  1100. $element['#previous'] = $element['#language'];
  1101. $element['#language'] = $form_langcode;
  1102. }
  1103. // Swap default values during form processing to avoid recursion. We
  1104. // try to act before any other callback so that the correct values are
  1105. // already in place for them.
  1106. if (!isset($element['#process'])) {
  1107. $element['#process'] = array();
  1108. }
  1109. array_unshift($element['#process'], 'entity_translation_prepare_element');
  1110. }
  1111. }
  1112. }
  1113. // Handle fields shared between translations when there is at least one
  1114. // translation available or a new one is being created.
  1115. if (!$handler->isNewEntity() && ($new_translation || count($translations->data) > 1)) {
  1116. $shared_access = $handler->getSharedFieldsAccess();
  1117. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  1118. foreach (field_info_instances($entity_type, $bundle) as $instance) {
  1119. $field_name = $instance['field_name'];
  1120. // Check if a field is part of the form array.
  1121. if (isset($form[$field_name])) {
  1122. $field = field_info_field($field_name);
  1123. // If access is not set or is granted we check whether the user has
  1124. // access to shared fields.
  1125. $form[$field_name]['#access'] = (!isset($form[$field_name]['#access']) || $form[$field_name]['#access']) && ($field['translatable'] || $shared_access);
  1126. $form[$field_name]['#multilingual'] = (boolean) $field['translatable'];
  1127. }
  1128. }
  1129. }
  1130. // If a translation is being created and no path alias exists for its
  1131. // language, by default an alias needs to be generated. The standard
  1132. // behavior is defaulting to FALSE when an entity already exists, hence we
  1133. // need to override it here.
  1134. if (module_exists('pathauto') && $handler->getSourceLanguage()) {
  1135. $entity->path['pathauto'] = TRUE;
  1136. }
  1137. }
  1138. }
  1139. /**
  1140. * Form element process callback.
  1141. */
  1142. function entity_translation_prepare_element($element, &$form_state) {
  1143. static $drupal_static_fast;
  1144. if (!isset($drupal_static_fast)) {
  1145. $drupal_static_fast = &drupal_static(__FUNCTION__, array(
  1146. 'source_forms' => array(),
  1147. 'source_form_states' => array(),
  1148. ));
  1149. }
  1150. $source_forms = &$drupal_static_fast['source_forms'];
  1151. $source_form_states = &$drupal_static_fast['source_form_states'];
  1152. $form = $form_state['complete form'];
  1153. $build_id = $form['#build_id'];
  1154. $source = $element['#source'];
  1155. $entity_type = $element['#entity_type'];
  1156. $id = $element['#entity_id'];
  1157. // Key the source form cache per entity type and entity id to allow for
  1158. // multiple entities on the same entity form.
  1159. if (!isset($source_forms[$build_id][$source][$entity_type][$id])) {
  1160. $source_form = array(
  1161. '#entity_translation_source_form' => TRUE,
  1162. '#parents' => $element['#form_parents'],
  1163. );
  1164. $source_form_state = $form_state;
  1165. field_attach_form($entity_type, $element['#entity'], $source_form, $source_form_state, $source);
  1166. $source_forms[$build_id][$source][$entity_type][$id] = &$source_form;
  1167. $source_form_states[$build_id][$source][$entity_type][$id] = &$source_form_state;
  1168. }
  1169. $source_form = &$source_forms[$build_id][$source][$entity_type][$id];
  1170. $source_form_state = $source_form_states[$build_id][$source][$entity_type][$id];
  1171. $langcode = $element['#language'];
  1172. $field_name = $element['#field_name'];
  1173. // If we are creating a new translation we have to change the form item
  1174. // language information from source to target language, this way the user can
  1175. // find the form items already populated with the source values while the
  1176. // field form element holds the correct language information.
  1177. if (isset($source_form[$field_name][$source])) {
  1178. $element[$langcode] = $source_form[$field_name][$source];
  1179. entity_translation_form_element_language_replace($element, $source, $langcode);
  1180. entity_translation_form_element_state_replace($element, $source_form[$field_name], $field_name, $source_form_state, $form_state);
  1181. unset($element[$element['#previous']]);
  1182. }
  1183. return $element;
  1184. }
  1185. /**
  1186. * Helper function. Sets the right values in $form_state['field'] when using
  1187. * source language values as defaults.
  1188. */
  1189. function entity_translation_form_element_state_replace($element, $source_element, $field_name, $source_form_state, &$form_state) {
  1190. if (isset($source_element['#language'])) {
  1191. $source = $source_element['#language'];
  1192. // Iterate through the form structure recursively.
  1193. foreach (element_children($element) as $key) {
  1194. if (isset($source_element[$key])) {
  1195. entity_translation_form_element_state_replace($element[$key], $source_element[$key], $key, $source_form_state, $form_state);
  1196. }
  1197. elseif (isset($source_element[$source])) {
  1198. entity_translation_form_element_state_replace($element[$key], $source_element[$source], $key, $source_form_state, $form_state);
  1199. }
  1200. }
  1201. if (isset($source_element[$source]['#field_parents'])) {
  1202. $source_parents = $source_element[$source]['#field_parents'];
  1203. $langcode = $element['#language'];
  1204. $parents = $element[$langcode]['#field_parents'];
  1205. $source_state = field_form_get_state($source_parents, $field_name, $source, $source_form_state);
  1206. drupal_alter('entity_translation_source_field_state', $source_state);
  1207. field_form_set_state($parents, $field_name, $langcode, $form_state, $source_state);
  1208. }
  1209. }
  1210. }
  1211. /**
  1212. * Helper function. Recursively replaces the source language with the given one.
  1213. */
  1214. function entity_translation_form_element_language_replace(&$element, $source, $langcode) {
  1215. // Iterate through the form structure recursively.
  1216. foreach (element_children($element) as $key) {
  1217. entity_translation_form_element_language_replace($element[$key], $source, $langcode);
  1218. }
  1219. // Replace specific occurrences of the source language with the target
  1220. // language.
  1221. foreach (element_properties($element) as $key) {
  1222. if ($key === '#language' && $element[$key] != LANGUAGE_NONE) {
  1223. $element[$key] = $langcode;
  1224. }
  1225. elseif ($key === '#parents' || $key === '#field_parents') {
  1226. foreach ($element[$key] as $delta => $value) {
  1227. if ($value === $source) {
  1228. $element[$key][$delta] = $langcode;
  1229. }
  1230. }
  1231. }
  1232. elseif ($key === '#limit_validation_errors') {
  1233. foreach ($element[$key] as $section => $section_value) {
  1234. foreach ($element[$key][$section] as $delta => $value) {
  1235. if ($value === $source) {
  1236. $element[$key][$section][$delta] = $langcode;
  1237. }
  1238. }
  1239. }
  1240. }
  1241. }
  1242. }
  1243. /**
  1244. * Adds visual clues about the translatability of a field to the given element.
  1245. *
  1246. * Field titles are appended with the string "Shared" for fields which are
  1247. * shared between different translations. Moreover fields receive a CSS class to
  1248. * distinguish between translatable and shared fields.
  1249. */
  1250. function entity_translation_element_translatability_clue($element) {
  1251. // Append language to element title.
  1252. if (empty($element['#multilingual'])) {
  1253. _entity_translation_element_title_append($element, ' (' . t('all languages') . ')');
  1254. }
  1255. // Add CSS class names.
  1256. if (!isset($element['#attributes'])) {
  1257. $element['#attributes'] = array();
  1258. }
  1259. if (!isset($element['#attributes']['class'])) {
  1260. $element['#attributes']['class'] = array();
  1261. }
  1262. $element['#attributes']['class'][] = 'entity-translation-' . (!empty($element['#multilingual']) ? 'field-translatable' : 'field-shared');
  1263. return $element;
  1264. }
  1265. /**
  1266. * Adds a callback function to the given FAPI element.
  1267. *
  1268. * Drupal core only adds default element callbacks if the respective handler
  1269. * type is not defined yet. This function ensures that our callback is only
  1270. * prepended/appended to the default set of callbacks instead of replacing it.
  1271. *
  1272. * @param $element
  1273. * The FAPI element.
  1274. * @param $type
  1275. * The callback type, e.g. '#pre_render' or '#process'.
  1276. * @param $function
  1277. * The name of the callback to add.
  1278. * @param $prepend
  1279. * Set to TRUE to add the new callback to the beginning of the existing set of
  1280. * callbacks, and set it to FALSE to append it at the end.
  1281. */
  1282. function _entity_translation_element_add_callback(&$element, $type, $function, $prepend = TRUE) {
  1283. // If handler type has not been set, add defaults from element_info().
  1284. if (!isset($element[$type])) {
  1285. $element_type = isset($element['#type']) ? $element['#type'] : 'markup';
  1286. $element_info = element_info($element_type);
  1287. $element[$type] = isset($element_info[$type]) ? $element_info[$type] : array();
  1288. }
  1289. if ($prepend) {
  1290. array_unshift($element[$type], $function);
  1291. }
  1292. else {
  1293. $element[$type][] = $function;
  1294. }
  1295. }
  1296. /**
  1297. * Appends the given $suffix string to the title of the given form element.
  1298. *
  1299. * If the given element does not have a #title attribute, the function is
  1300. * recursively applied to child elements.
  1301. */
  1302. function _entity_translation_element_title_append(&$element, $suffix) {
  1303. static $fapi_title_elements;
  1304. // Elements which can have a #title attribute according to FAPI Reference.
  1305. if (!isset($fapi_title_elements)) {
  1306. $fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight'));
  1307. }
  1308. // Update #title attribute for all elements that are allowed to have a #title
  1309. // attribute according to the Form API Reference. The reason for this check
  1310. // is because some elements have a #title attribute even though it is not
  1311. // rendered, e.g. field containers.
  1312. if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
  1313. $element['#title'] .= $suffix;
  1314. }
  1315. // If this is a multi-valued field, apply the suffix to the container.
  1316. elseif (isset($element['#title']) && isset($element['#cardinality']) && $element['#cardinality'] != 1) {
  1317. $element['#title'] .= $suffix;
  1318. }
  1319. // If the current element does not have a (valid) title, try child elements.
  1320. elseif ($children = element_children($element)) {
  1321. foreach ($children as $delta) {
  1322. _entity_translation_element_title_append($element[$delta], $suffix);
  1323. }
  1324. }
  1325. // If there are no children, fall back to the current #title attribute if it
  1326. // exists.
  1327. elseif (isset($element['#title'])) {
  1328. $element['#title'] .= $suffix;
  1329. }
  1330. }
  1331. /**
  1332. * Implements hook_form_alter().
  1333. */
  1334. function entity_translation_form_alter(&$form, &$form_state) {
  1335. if ($info = entity_translation_edit_form_info($form, $form_state)) {
  1336. $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
  1337. if (entity_translation_enabled($info['entity type'], $info['entity'])) {
  1338. if (!$handler->isNewEntity()) {
  1339. $handler->entityForm($form, $form_state);
  1340. $translations = $handler->getTranslations();
  1341. $form_langcode = $handler->getActiveLanguage();
  1342. if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
  1343. // Hide shared form elements if the user is not allowed to edit them.
  1344. $handler->entityFormSharedElements($form);
  1345. }
  1346. }
  1347. else {
  1348. $handler->entityFormLanguageWidget($form, $form_state);
  1349. }
  1350. // We need to process the posted form as early as possible to update the
  1351. // form language value.
  1352. array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
  1353. }
  1354. // We might have an entity form for an entity or a bundle not enabled for
  1355. // translation. In this case we might need to deal with entity and field
  1356. // languages anyway, since fields may be shared among different bundles and
  1357. // entity types.
  1358. else {
  1359. $handler->entityFormLanguageWidget($form, $form_state);
  1360. }
  1361. }
  1362. }
  1363. /**
  1364. * Submit handler for the source language selector.
  1365. */
  1366. function entity_translation_entity_form_source_language_submit($form, &$form_state) {
  1367. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1368. $langcode = $form_state['values']['source_language']['language'];
  1369. $path = "{$handler->getEditPath()}/add/$langcode/{$handler->getActiveLanguage()}";
  1370. $options = array();
  1371. if (isset($_GET['destination'])) {
  1372. $options['query'] = drupal_get_destination();
  1373. unset($_GET['destination']);
  1374. }
  1375. $form_state['redirect'] = array($path, $options);
  1376. $languages = language_list();
  1377. drupal_set_message(t('Source translation set to: %language', array('%language' => t($languages[$langcode]->name))));
  1378. }
  1379. /**
  1380. * Submit handler for the translation deletion.
  1381. */
  1382. function entity_translation_entity_form_delete_translation_submit($form, &$form_state) {
  1383. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1384. $path = "{$handler->getTranslatePath()}/delete/{$handler->getActiveLanguage()}";
  1385. $options = array();
  1386. if (isset($_GET['destination'])) {
  1387. $options['query'] = drupal_get_destination();
  1388. unset($_GET['destination']);
  1389. }
  1390. $form_state['redirect'] = array($path, $options);
  1391. }
  1392. /**
  1393. * Validation handler for the entity edit form.
  1394. */
  1395. function entity_translation_entity_form_validate($form, &$form_state) {
  1396. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1397. if (!empty($handler)) {
  1398. $handler->entityFormValidate($form, $form_state);
  1399. }
  1400. }
  1401. /**
  1402. * Validation handler for the entity language widget.
  1403. */
  1404. function entity_translation_entity_form_language_update($element, &$form_state, $form) {
  1405. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1406. // Ensure the handler form language match the actual one. This is mainly
  1407. // needed when responding to an AJAX request where the languages cannot be set
  1408. // from the usual page callback.
  1409. if (!empty($form_state['entity_translation']['form_langcode'])) {
  1410. $handler->setActiveLanguage($form_state['entity_translation']['form_langcode']);
  1411. }
  1412. // When responding to an AJAX request we should ignore any change in the
  1413. // language widget as it may alter the field language expected by the AJAX
  1414. // callback.
  1415. if (empty($form_state['triggering_element']['#ajax'])) {
  1416. $handler->entityFormLanguageWidgetSubmit($form, $form_state);
  1417. }
  1418. }
  1419. /**
  1420. * Submit handler for the entity deletion.
  1421. */
  1422. function entity_translation_entity_form_submit($form, &$form_state) {
  1423. if ($form_state['clicked_button']['#value'] == t('Delete')) {
  1424. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1425. if (count($handler->getTranslations()->data) > 1) {
  1426. $info = entity_get_info($form['#entity_type']);
  1427. drupal_set_message(t('This will delete all the @entity_type translations.', array('@entity_type' => drupal_strtolower($info['label']))), 'warning');
  1428. }
  1429. }
  1430. }
  1431. /**
  1432. * Implementation of hook_field_attach_submit().
  1433. *
  1434. * Mark translations as outdated if the submitted value is true.
  1435. */
  1436. function entity_translation_field_attach_submit($entity_type, $entity, $form, &$form_state) {
  1437. if (($handler = entity_translation_entity_form_get_handler($form, $form_state)) && entity_translation_enabled($entity_type, $entity)) {
  1438. // Update the wrapped entity with the submitted values.
  1439. $handler->setEntity($entity);
  1440. $handler->entityFormSubmit($form, $form_state);
  1441. // Process in-place translations for the taxonomy autocomplete widget.
  1442. entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, $form_state);
  1443. }
  1444. }
  1445. /**
  1446. * Implements hook_menu_local_tasks_alter().
  1447. */
  1448. function entity_translation_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  1449. // When displaying the main edit form, we need to craft an additional level of
  1450. // local tasks for each available translation.
  1451. $handler = entity_translation_get_handler();
  1452. if (!empty($handler) && $handler->isEntityForm() && $handler->getLanguage() != LANGUAGE_NONE && drupal_multilingual()) {
  1453. $handler->localTasksAlter($data, $router_item, $root_path);
  1454. }
  1455. }
  1456. /**
  1457. * Preprocess variables for 'page.tpl.php'.
  1458. */
  1459. function entity_translation_preprocess_page(&$variables) {
  1460. if (!empty($variables['tabs']['#secondary'])) {
  1461. $language_tabs = array();
  1462. foreach ($variables['tabs']['#secondary'] as $index => $tab) {
  1463. if (!empty($tab['#language_tab'])) {
  1464. $language_tabs[] = $tab;
  1465. unset($variables['tabs']['#secondary'][$index]);
  1466. }
  1467. }
  1468. if (!empty($language_tabs)) {
  1469. if (count($variables['tabs']['#secondary']) <= 1) {
  1470. $variables['tabs']['#secondary'] = $language_tabs;
  1471. }
  1472. else {
  1473. // If secondary tabs are already defined we need to add another level
  1474. // and wrap it so that it will be positioned on its own row.
  1475. $variables['tabs']['#secondary']['#language_tabs'] = $language_tabs;
  1476. $variables['tabs']['#secondary']['#pre_render']['entity_translation'] = 'entity_translation_language_tabs_render';
  1477. }
  1478. }
  1479. }
  1480. }
  1481. /**
  1482. * Pre render callback.
  1483. *
  1484. * Appends the language tabs to the current local tasks area.
  1485. */
  1486. function entity_translation_language_tabs_render($element) {
  1487. $build = array(
  1488. '#theme' => 'menu_local_tasks',
  1489. '#theme_wrappers' => array('entity_translation_language_tabs'),
  1490. '#secondary' => $element['#language_tabs'],
  1491. '#attached' => array(
  1492. 'css' => array(drupal_get_path('module', 'entity_translation') . '/entity-translation.css'),
  1493. ),
  1494. );
  1495. $element['#suffix'] .= drupal_render($build);
  1496. return $element;
  1497. }
  1498. /**
  1499. * Theme wrapper for the entity translation language tabs.
  1500. */
  1501. function theme_entity_translation_language_tabs($variables) {
  1502. return '<div class="entity-translation-language-tabs">' . $variables['element']['#children'] . '</div>';
  1503. }
  1504. /**
  1505. * Implements hook_form_FORM_ID_alter().
  1506. *
  1507. * Adds an option to enable field synchronization.
  1508. * Enable a selector to choose whether a field is translatable.
  1509. */
  1510. function entity_translation_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
  1511. $instance = $form['#instance'];
  1512. $entity_type = $instance['entity_type'];
  1513. $field_name = $instance['field_name'];
  1514. $field = field_info_field($field_name);
  1515. if (!empty($field['settings']['entity_translation_sync']) && field_is_translatable($entity_type, $field)) {
  1516. $form['instance']['settings']['entity_translation_sync'] = array(
  1517. '#prefix' => '<label>' . t('Field synchronization') . '</label>',
  1518. '#type' => 'checkbox',
  1519. '#title' => t('Enable field synchronization'),
  1520. '#description' => t('Check this option if you wish to synchronize the value of this field across its translations.'),
  1521. '#default_value' => !empty($instance['settings']['entity_translation_sync']),
  1522. );
  1523. }
  1524. $translatable = $field['translatable'];
  1525. $label = t('Field translation');
  1526. $title = t('Users may translate all occurrences of this field:') . _entity_translation_field_desc($field);
  1527. $form_state['field_has_data'] = field_has_data($field);
  1528. if ($form_state['field_has_data']) {
  1529. $path = "admin/config/regional/entity_translation/translatable/$field_name";
  1530. $status = $translatable ? $title : (t('All occurrences of this field are untranslatable:') . _entity_translation_field_desc($field));
  1531. $link_title = !$translatable ? t('Enable translation') : t('Disable translation');
  1532. $form['field']['translatable'] = array(
  1533. '#prefix' => '<div class="translatable"><label>' . $label . '</label>',
  1534. '#suffix' => '</div>',
  1535. 'message' => array(
  1536. '#markup' => $status . ' ',
  1537. ),
  1538. 'link' => array(
  1539. '#type' => 'link',
  1540. '#title' => $link_title,
  1541. '#href' => $path,
  1542. '#options' => array('query' => drupal_get_destination()),
  1543. '#access' => user_access('toggle field translatability'),
  1544. ),
  1545. );
  1546. }
  1547. else {
  1548. $form['field']['translatable'] = array(
  1549. '#prefix' => '<label>' . $label . '</label>',
  1550. '#type' => 'checkbox',
  1551. '#title' => $title,
  1552. '#default_value' => $translatable,
  1553. );
  1554. }
  1555. $function = 'entity_translation_form_field_ui_field_edit_' . $instance['widget']['type'] . '_form_alter';
  1556. if (function_exists($function)) {
  1557. $function($form, $form_state);
  1558. }
  1559. }
  1560. /**
  1561. * Returns a human-readable, localized, bullet list of instances of a field.
  1562. *
  1563. * @param field
  1564. * A field data structure.
  1565. *
  1566. * @return
  1567. * A themed list of field instances with the bundle they are attached to.
  1568. */
  1569. function _entity_translation_field_desc($field) {
  1570. $instances = array();
  1571. foreach ($field['bundles'] as $entity_type => $bundle_names) {
  1572. $entity_type_info = entity_get_info($entity_type);
  1573. foreach ($bundle_names as $bundle_name) {
  1574. $instance_info = field_info_instance($entity_type, $field['field_name'], $bundle_name);
  1575. $instances[] = t('@instance_label in %entity_label', array('@instance_label' => $instance_info['label'], '%entity_label' => $entity_type_info['bundles'][$bundle_name]['label']));
  1576. }
  1577. }
  1578. return theme('item_list', array('items' => $instances));
  1579. }
  1580. /**
  1581. * Determines whether the given entity type is translatable.
  1582. *
  1583. * @param $entity_type
  1584. * The entity type enabled for translation.
  1585. * @param $entity
  1586. * (optional) The entity belonging to the bundle enabled for translation. A
  1587. * bundle name can alternatively be passed. If an empty value is passed the
  1588. * bundle-level check is skipped. Defaults to NULL.
  1589. * @param $skip_handler
  1590. * (optional) A boolean indicating whether skip checking if the entity type is
  1591. * registered as a field translation handler. Defaults to FALSE.
  1592. */
  1593. function entity_translation_enabled($entity_type, $entity = NULL, $skip_handler = FALSE) {
  1594. $enabled_types = variable_get('entity_translation_entity_types', array());
  1595. $enabled = !empty($enabled_types[$entity_type]) && ($skip_handler || field_has_translation_handler($entity_type, 'entity_translation'));
  1596. // If the entity type is not enabled or we are not checking bundle status, we
  1597. // have a result.
  1598. if (!$enabled || !isset($entity)) {
  1599. return $enabled;
  1600. }
  1601. // Determine the bundle to check for translatability.
  1602. $bundle = FALSE;
  1603. if (is_object($entity)) {
  1604. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  1605. }
  1606. elseif (is_string($entity)) {
  1607. $bundle = $entity;
  1608. }
  1609. return $bundle && entity_translation_enabled_bundle($entity_type, $bundle);
  1610. }
  1611. /**
  1612. * Determines whether the given entity bundle is translatable.
  1613. *
  1614. * NOTE: Does not check for whether the entity type is translatable.
  1615. * Consider using entity_translation_enabled() instead.
  1616. *
  1617. * @param $entity_type
  1618. * The entity type the bundle to be checked belongs to.
  1619. * @param $bundle
  1620. * The name of the bundle to be checked.
  1621. */
  1622. function entity_translation_enabled_bundle($entity_type, $bundle) {
  1623. $info = entity_get_info($entity_type);
  1624. $bundle_callback = isset($info['translation']['entity_translation']['bundle callback']) ? $info['translation']['entity_translation']['bundle callback'] : FALSE;
  1625. return empty($bundle_callback) || call_user_func($bundle_callback, $bundle);
  1626. }
  1627. /**
  1628. * Return the entity translation settings for the given entity type and bundle.
  1629. */
  1630. function entity_translation_settings($entity_type, $bundle) {
  1631. $settings = variable_get('entity_translation_settings_' . $entity_type . '__' . $bundle, array());
  1632. if (empty($settings)) {
  1633. $info = entity_get_info($entity_type);
  1634. if (!empty($info['translation']['entity_translation']['default settings'])) {
  1635. $settings = $info['translation']['entity_translation']['default settings'];
  1636. }
  1637. }
  1638. $settings += array(
  1639. 'default_language' => ENTITY_TRANSLATION_LANGUAGE_DEFAULT,
  1640. 'hide_language_selector' => TRUE,
  1641. 'exclude_language_none' => FALSE,
  1642. 'lock_language' => FALSE,
  1643. 'shared_fields_original_only' => FALSE,
  1644. );
  1645. return $settings;
  1646. }
  1647. /**
  1648. * Entity language callback.
  1649. *
  1650. * This callback changes the entity language from the actual one to the active
  1651. * language. This overriding allows to obtain language dependent form widgets
  1652. * where multilingual values are supported (e.g. field or path alias widgets)
  1653. * even if the code was not originally written with supporting multiple values
  1654. * per language in mind.
  1655. *
  1656. * The main drawback of this approach is that code needing to access the actual
  1657. * language in the entity form build/validation/submit workflow cannot rely on
  1658. * the entity_language() function. On the other hand in these scenarios assuming
  1659. * the presence of Entity translation should be safe, thus developers can rely
  1660. * on the EntityTranslationHandlerInterface::getLanguage() method.
  1661. *
  1662. * @param string $entity_type
  1663. * The the type of the entity.
  1664. * @param object $entity
  1665. * The entity whose language has to be returned.
  1666. *
  1667. * @return string
  1668. * A valid language code.
  1669. */
  1670. function entity_translation_language($entity_type, $entity) {
  1671. $handler = entity_translation_get_handler($entity_type, $entity);
  1672. if (!$handler) {
  1673. return LANGUAGE_NONE;
  1674. }
  1675. if (entity_translation_enabled($entity_type, $entity)) {
  1676. $langcode = $handler->getActiveLanguage();
  1677. return $langcode ? $langcode : $handler->getLanguage();
  1678. }
  1679. else {
  1680. return $handler->getLanguage();
  1681. }
  1682. }
  1683. /**
  1684. * Translation handler factory.
  1685. *
  1686. * @param $entity_type
  1687. * (optional) The type of $entity; e.g. 'node' or 'user'.
  1688. * @param $entity
  1689. * (optional) The entity to be translated. A bundle name may be passed to
  1690. * instantiate an empty entity.
  1691. *
  1692. * @return EntityTranslationHandlerInterface
  1693. * A class implementing EntityTranslationHandlerInterface.
  1694. */
  1695. function entity_translation_get_handler($entity_type = NULL, $entity = NULL) {
  1696. $factory = EntityTranslationHandlerFactory::getInstance();
  1697. return empty($entity) ? $factory->getLastHandler($entity_type) : $factory->getHandler($entity_type, $entity);
  1698. }
  1699. /**
  1700. * Returns the translation handler wrapping the entity being edited.
  1701. *
  1702. * @param $form
  1703. * The entity form.
  1704. * @param $form_state
  1705. * A keyed array containing the current state of the form.
  1706. *
  1707. * @return EntityTranslationHandlerInterface
  1708. * A class implementing EntityTranslationHandlerInterface.
  1709. */
  1710. function entity_translation_entity_form_get_handler($form, $form_state) {
  1711. $handler = FALSE;
  1712. if ($info = entity_translation_edit_form_info($form, $form_state)) {
  1713. $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
  1714. }
  1715. return $handler;
  1716. }
  1717. /**
  1718. * Returns the translation handler associated to the currently submitted form.
  1719. *
  1720. * @return EntityTranslationHandlerInterface
  1721. * A translation handler instance if available, FALSE oterwise.
  1722. *
  1723. * @deprecated This is no longer used and will be removed in the first stable
  1724. * release.
  1725. */
  1726. function entity_translation_current_form_get_handler() {
  1727. $handler = FALSE;
  1728. if (!empty($_POST['form_build_id'])) {
  1729. $form_state = form_state_defaults();
  1730. if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
  1731. $handler = entity_translation_entity_form_get_handler($form, $form_state);
  1732. }
  1733. }
  1734. return $handler;
  1735. }
  1736. /**
  1737. * Returns an array of edit form info as defined in hook_entity_info().
  1738. *
  1739. * @param $form
  1740. * The entity edit form.
  1741. * @param $form_state
  1742. * The entity edit form state.
  1743. *
  1744. * @return
  1745. * An edit form info array containing the entity to be translated in the
  1746. * 'entity' key.
  1747. */
  1748. function entity_translation_edit_form_info($form, $form_state) {
  1749. $info = FALSE;
  1750. $entity_type = isset($form['#entity_type']) && is_string($form['#entity_type']) ? $form['#entity_type'] : FALSE;
  1751. if ($entity_type) {
  1752. $entity_info = entity_get_info($form['#entity_type']);
  1753. if (!empty($entity_info['translation']['entity_translation']['edit form'])) {
  1754. $entity_keys = explode('][', $entity_info['translation']['entity_translation']['edit form']);
  1755. $key_exists = FALSE;
  1756. $entity = drupal_array_get_nested_value($form_state, $entity_keys, $key_exists);
  1757. if ($key_exists) {
  1758. $info = array(
  1759. 'entity type' => $form['#entity_type'],
  1760. 'entity' => (object) $entity,
  1761. );
  1762. }
  1763. }
  1764. }
  1765. return $info;
  1766. }
  1767. /**
  1768. * Checks whether an entity translation is accessible.
  1769. *
  1770. * @param $translation
  1771. * An array representing an entity translation.
  1772. *
  1773. * @return
  1774. * TRUE if the current user is allowed to view the translation.
  1775. */
  1776. function entity_translation_access($entity_type, $translation) {
  1777. return $translation['status'] || user_access('translate any entity') || user_access("translate $entity_type entities");
  1778. }
  1779. /**
  1780. * Returns the set of languages available for translations.
  1781. */
  1782. function entity_translation_languages($entity_type = NULL, $entity = NULL) {
  1783. if (isset($entity) && $entity_type == 'node' && module_exists('i18n_node')) {
  1784. // @todo Inherit i18n language settings.
  1785. }
  1786. elseif (variable_get('entity_translation_languages_enabled', FALSE)) {
  1787. $languages = language_list('enabled');
  1788. return $languages[1];
  1789. }
  1790. return language_list();
  1791. }
  1792. /**
  1793. * Implements hook_views_api().
  1794. */
  1795. function entity_translation_views_api() {
  1796. return array(
  1797. 'api' => 3,
  1798. 'path' => drupal_get_path('module', 'entity_translation') . '/views',
  1799. );
  1800. }
  1801. /**
  1802. * Implements hook_uuid_entities_features_export_entity_alter().
  1803. */
  1804. function entity_translation_uuid_entities_features_export_entity_alter($entity, $entity_type) {
  1805. // We do not need to export most of the keys:
  1806. // - The entity type is determined from the entity the translations are
  1807. // attached to.
  1808. // - The entity id will change from one site to another.
  1809. // - The user id needs to be removed because it will change as well.
  1810. // - Created and changed could be left but the UUID module removes created and
  1811. // changed values from the entities it exports, hence we do the same for
  1812. // consistency.
  1813. if (entity_translation_enabled($entity_type, $entity)) {
  1814. $fields = array('entity_type', 'entity_id', 'uid', 'created', 'changed');
  1815. $handler = entity_translation_get_handler($entity_type, $entity);
  1816. $translations = $handler->getTranslations();
  1817. if ($translations && isset($translations->data)) {
  1818. foreach ($translations->data as &$translation) {
  1819. foreach ($fields as $field) {
  1820. unset($translation[$field]);
  1821. }
  1822. }
  1823. }
  1824. }
  1825. }
  1826. /**
  1827. * Implements hook_entity_uuid_presave().
  1828. */
  1829. function entity_translation_entity_uuid_presave(&$entity, $entity_type) {
  1830. // UUID exports entities as arrays, therefore we need to cast the translations
  1831. // array back into an object.
  1832. $entity_info = entity_get_info($entity_type);
  1833. if (isset($entity_info['entity keys']) && isset($entity_info['entity keys']['translations'])) {
  1834. $key = $entity_info['entity keys']['translations'];
  1835. if (isset($entity->{$key})) {
  1836. $entity->{$key} = (object) $entity->{$key};
  1837. }
  1838. }
  1839. }
  1840. /**
  1841. * Implements hook_pathauto_alias_alter().
  1842. *
  1843. * When bulk-updating aliases for nodes automatically create a path for every
  1844. * translation.
  1845. */
  1846. function entity_translation_pathauto_alias_alter(&$alias, array &$context) {
  1847. $info = entity_get_info();
  1848. $entity_type = $context['module'];
  1849. // Ensure that we are dealing with a bundle having entity translation enabled.
  1850. if (in_array($context['op'], array('bulkupdate', 'update')) && !empty($info[$entity_type]['token type']) && !empty($context['data'][$info[$entity_type]['token type']])) {
  1851. $entity = $context['data'][$info[$entity_type]['token type']];
  1852. if (entity_translation_enabled($entity_type, $entity)) {
  1853. $translations = entity_translation_get_handler($entity_type, $entity)->getTranslations();
  1854. // To avoid infinite recursion, remember the starting language.
  1855. static $pathauto_start_language;
  1856. if (!$pathauto_start_language) {
  1857. $pathauto_start_language = $context['language'];
  1858. }
  1859. if ($context['language'] == $pathauto_start_language && $context['language'] != LANGUAGE_NONE) {
  1860. foreach ($translations->data as $language => $translation) {
  1861. // We already have an alias for the starting language, so let's not
  1862. // create another one.
  1863. if ($language == $pathauto_start_language) {
  1864. continue;
  1865. }
  1866. pathauto_create_alias($entity_type, $context['op'], $context['source'], $context['data'], $context['type'], $language);
  1867. }
  1868. }
  1869. }
  1870. }
  1871. }
  1872. /**
  1873. * Implements hook_entity_translation_delete().
  1874. */
  1875. function path_entity_translation_delete($entity_type, $entity, $langcode) {
  1876. // Remove any existing path alias for the removed translation.
  1877. $handler = entity_translation_get_handler($entity_type, $entity);
  1878. path_delete(array('source' => $handler->getViewPath(), 'language' => $langcode));
  1879. }
  1880. /**
  1881. * Wrapper for entity_save().
  1882. *
  1883. * @param $entity_type
  1884. * The entity type.
  1885. * @param $entity
  1886. * The entity object.
  1887. */
  1888. function entity_translation_entity_save($entity_type, $entity) {
  1889. // Entity module isn't required, but use it if it's available.
  1890. if (module_exists('entity')) {
  1891. entity_save($entity_type, $entity);
  1892. }
  1893. // Fall back to field_attach_* functions otherwise.
  1894. else {
  1895. field_attach_presave($entity_type, $entity);
  1896. field_attach_update($entity_type, $entity);
  1897. }
  1898. }
  1899. /**
  1900. * Implements hook_field_attach_prepare_translation_alter().
  1901. */
  1902. function entity_translation_field_attach_prepare_translation_alter(&$entity, $context) {
  1903. $handler = entity_translation_get_handler('node', $entity);
  1904. $handler->setActiveLanguage($context['langcode']);
  1905. }