entity_translation.module 76 KB

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