i18n_field.module 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <?php
  2. /**
  3. * @file
  4. * Internationalization (i18n) module - Field handling
  5. *
  6. * For string keys we use:
  7. * - field:[field_name]:[bundle]:property, when it is an instance property (linked to bundle)
  8. * - field:[field_name]:#property..., when it is a field property (that may have multiple values)
  9. */
  10. /**
  11. * Implements hook_menu().
  12. */
  13. function i18n_field_menu() {
  14. $items = array();
  15. // Ensure the following is not executed until field_bundles is working and
  16. // tables are updated. Needed to avoid errors on initial installation.
  17. if (!module_exists('field_ui') || defined('MAINTENANCE_MODE')) {
  18. return $items;
  19. }
  20. // Create tabs for all possible bundles. From field_ui_menu().
  21. foreach (entity_get_info() as $entity_type => $entity_info) {
  22. if ($entity_info['fieldable']) {
  23. foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
  24. if (isset($bundle_info['admin'])) {
  25. // Extract path information from the bundle.
  26. $path = $bundle_info['admin']['path'];
  27. // Different bundles can appear on the same path (e.g. %node_type and
  28. // %comment_node_type). To allow field_ui_menu_load() to extract the
  29. // actual bundle object from the translated menu router path
  30. // arguments, we need to identify the argument position of the bundle
  31. // name string ('bundle argument') and pass that position to the menu
  32. // loader. The position needs to be casted into a string; otherwise it
  33. // would be replaced with the bundle name string.
  34. if (isset($bundle_info['admin']['bundle argument'])) {
  35. $bundle_arg = $bundle_info['admin']['bundle argument'];
  36. $bundle_pos = (string) $bundle_arg;
  37. }
  38. else {
  39. $bundle_arg = $bundle_name;
  40. $bundle_pos = '0';
  41. }
  42. // This is the position of the %field_ui_menu placeholder in the
  43. // items below.
  44. $field_position = count(explode('/', $path)) + 1;
  45. // Extract access information, providing defaults.
  46. $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
  47. $access += array(
  48. 'access callback' => 'user_access',
  49. 'access arguments' => array('administer site configuration'),
  50. );
  51. $items["$path/fields/%field_ui_menu/translate"] = array(
  52. 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
  53. 'title' => 'Translate',
  54. 'page callback' => 'i18n_field_page_translate',
  55. 'page arguments' => array($field_position),
  56. 'file' => 'i18n_field.pages.inc',
  57. 'type' => MENU_LOCAL_TASK,
  58. ) + $access;
  59. $items["$path/fields/%field_ui_menu/translate/%i18n_language"] = array(
  60. 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
  61. 'title' => 'Instance',
  62. 'page callback' => 'i18n_field_page_translate',
  63. 'page arguments' => array($field_position, $field_position + 2),
  64. 'file' => 'i18n_field.pages.inc',
  65. 'type' => MENU_CALLBACK,
  66. ) + $access;
  67. }
  68. }
  69. }
  70. }
  71. return $items;
  72. }
  73. /**
  74. * Implements hook_hook_info().
  75. */
  76. function i18n_field_hook_info() {
  77. $hooks['i18n_field_info'] = array(
  78. 'group' => 'i18n',
  79. );
  80. return $hooks;
  81. }
  82. /**
  83. * Implements hook_field_attach_form().
  84. *
  85. * After the form fields are built. Translate title and description for fields with multiple values.
  86. */
  87. function i18n_field_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
  88. // Determine the list of instances to iterate on.
  89. list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  90. $instances = field_info_instances($entity_type, $bundle);
  91. foreach ($instances as $field_name => $instance) {
  92. if (isset($form[$field_name])) {
  93. $langcode = $form[$field_name]['#language'];
  94. $field = &$form[$field_name];
  95. // Note: cardinality for unlimited fields is -1
  96. if (isset($field[$langcode]['#cardinality']) && $field[$langcode]['#cardinality'] != 1) {
  97. $translated = i18n_string_object_translate('field_instance', $instance);
  98. if (!empty($field[$langcode]['#title'])) {
  99. $field[$langcode]['#title'] = $translated['label'];
  100. }
  101. if (!empty($field[$langcode]['#description'])) {
  102. $field[$langcode]['#description'] = $translated['description'];
  103. }
  104. }
  105. }
  106. }
  107. }
  108. /**
  109. * Implements hook_field_formatter_info().
  110. */
  111. function i18n_field_field_formatter_info() {
  112. $types = array();
  113. foreach (i18n_field_type_info() as $type => $info) {
  114. if (!empty($info['translate_options'])) {
  115. $types[] = $type;
  116. }
  117. }
  118. return array(
  119. 'i18n_list_default' => array(
  120. 'label' => t('Default translated'),
  121. 'field types' => $types,
  122. ),
  123. );
  124. }
  125. /**
  126. * Implements hook_field_formatter_view().
  127. */
  128. function i18n_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  129. $element = array();
  130. switch ($display['type']) {
  131. case 'i18n_list_default':
  132. if (($translate = i18n_field_type_info($field['type'], 'translate_options'))) {
  133. $allowed_values = $translate($field);
  134. }
  135. else {
  136. // Defaults to list_default behavior
  137. $allowed_values = list_allowed_values($field);
  138. }
  139. foreach ($items as $delta => $item) {
  140. if (isset($allowed_values[$item['value']])) {
  141. $output = field_filter_xss($allowed_values[$item['value']]);
  142. }
  143. else {
  144. // If no match was found in allowed values, fall back to the key.
  145. $output = field_filter_xss($item['value']);
  146. }
  147. $element[$delta] = array('#markup' => $output);
  148. }
  149. break;
  150. }
  151. return $element;
  152. }
  153. /**
  154. * Implements hook_field_widget_form_alter().
  155. *
  156. * Translate:
  157. * - Title (label)
  158. * - Description (help)
  159. * - Default value
  160. * - List options
  161. */
  162. function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) {
  163. global $language;
  164. // Don't translate if the widget is being shown on the field edit form.
  165. if ($form_state['build_info']['form_id'] == 'field_ui_field_edit_form') {
  166. return;
  167. }
  168. // Skip if we are missing any of the parameters
  169. if (empty($context['field']) || empty($context['instance']) || empty($context['langcode'])) {
  170. return;
  171. }
  172. $field = $context['field'];
  173. $instance = $context['instance'];
  174. $langcode = $context['langcode'];
  175. // Get the element to alter. Account for inconsistencies in how the element
  176. // is built for different field types.
  177. if (isset($element[0]) && count($element) == 1) {
  178. // Single-value file fields and image fields.
  179. $alter_element = &$element[0];
  180. }
  181. elseif ($field['type'] == 'url' && $field['module'] == 'url' && $field['cardinality'] == 1) {
  182. $alter_element = &$element;
  183. }
  184. elseif (isset($element['value'])) {
  185. // Number fields. Single-value text fields.
  186. $alter_element = &$element['value'];
  187. }
  188. elseif ($field['type'] == 'entityreference' && isset($element['target_id'])) {
  189. // Entityreference fields using the entityreference_autocomplete widget.
  190. $alter_element = &$element['target_id'];
  191. }
  192. elseif ($field['type'] == 'node_reference' && isset($element['nid'])) {
  193. // The node_reference fields using the entityreference_autocomplete widget.
  194. $alter_element = &$element['nid'];
  195. }
  196. else {
  197. // All other fields.
  198. $alter_element = &$element;
  199. }
  200. // If a subelement has the same title as the parent, translate it instead.
  201. // Allows fields such as email and commerce_price to be translated.
  202. foreach (element_get_visible_children($element) as $key) {
  203. $single_value = ($field['cardinality'] == 1);
  204. $has_title = (isset($element['#title']) && isset($element[$key]['#title']));
  205. if ($single_value && $has_title && $element[$key]['#title'] == $element['#title']) {
  206. $alter_element = &$element[$key];
  207. break;
  208. }
  209. }
  210. // The field language may affect some variables (default) but not others (description will be in current page language)
  211. $i18n_langcode = empty($alter_element['#language']) || $alter_element['#language'] == LANGUAGE_NONE ? $language->language : $alter_element['#language'];
  212. // Translate instance to current page language and set to form_state
  213. // so it will be used for validation messages later.
  214. $instance_current = i18n_string_object_translate('field_instance', $instance);
  215. if (isset($form_state['field'][$instance['field_name']][$langcode]['instance'])) {
  216. $form_state['field'][$instance['field_name']][$langcode]['instance'] = $instance_current;
  217. }
  218. // Translate field title if set and it is the default one.
  219. if (!empty($instance_current['label']) && $instance_current['label'] != $instance['label']) {
  220. if (!empty($alter_element['#title']) && $alter_element['#title'] == check_plain($instance['label'])) {
  221. $alter_element['#title'] = check_plain($instance_current['label']);
  222. }
  223. }
  224. // Translate field description if set and it is the default one.
  225. if (!empty($instance_current['description']) && $instance_current['description'] != $instance['description']) {
  226. if (!empty($alter_element['#description'])) {
  227. // Allow single-value file fields and image fields to have their
  228. // descriptions translated. file_field_widget_form() passes the
  229. // description through theme('file_upload_help'), so i18n_field
  230. // must do the same.
  231. $filefield = in_array($field['type'], array('file', 'image'));
  232. $single_value = ($field['cardinality'] == 1);
  233. $no_default = empty($alter_element['#default_value']['fid']);
  234. if ($filefield && $single_value && $no_default) {
  235. $help_variables = array(
  236. 'description' => field_filter_xss($instance['description']),
  237. 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
  238. );
  239. $original_description = theme('file_upload_help', $help_variables);
  240. if ($alter_element['#description'] == $original_description) {
  241. $help_variables = array(
  242. 'description' => field_filter_xss($instance_current['description']),
  243. 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
  244. );
  245. $alter_element['#description'] = theme('file_upload_help', $help_variables);
  246. }
  247. }
  248. elseif ($alter_element['#description'] == field_filter_xss($instance['description'])) {
  249. $alter_element['#description'] = field_filter_xss($instance_current['description']);
  250. }
  251. }
  252. }
  253. // Translate list options.
  254. $has_options = (!empty($alter_element['#options']) || $field['type'] == 'list_boolean');
  255. $has_allowed_values = !empty($field['settings']['allowed_values']);
  256. $translate = i18n_field_type_info($field['type'], 'translate_options');
  257. if ($has_options && $has_allowed_values && $translate) {
  258. $alter_element['#options'] = $translate($field, $i18n_langcode);
  259. if (isset($alter_element['#properties']) && !empty($alter_element['#properties']['empty_option'])) {
  260. $label = theme('options_none', array('instance' => $instance, 'option' => $alter_element['#properties']['empty_option']));
  261. $alter_element['#options'] = array('_none' => $label) + $alter_element['#options'];
  262. }
  263. // Translate list_boolean fields using the checkboxes widget.
  264. if (!empty($alter_element['#title']) && $field['type'] == 'list_boolean' && !empty($alter_element['#on_value'])) {
  265. $on_value = $alter_element['#on_value'];
  266. $alter_element['#options'];
  267. $alter_element['#title'] = $alter_element['#options'][$on_value];
  268. // For using label instead of "On value".
  269. if ($instance['widget']['settings']['display_label']) {
  270. $alter_element['#title'] = $instance_current['label'];
  271. }
  272. }
  273. }
  274. // Check for more parameters, skip this part if missing.
  275. if (!isset($context['delta']) || !isset($context['items'])) {
  276. return;
  277. }
  278. $delta = $context['delta'];
  279. $items = $context['items'];
  280. // Translate default value.
  281. $has_default_value = (isset($alter_element['#default_value']) && !empty($instance['default_value'][$delta]['value']));
  282. $storage_has_value = !empty($items[$delta]['value']);
  283. $translate = i18n_field_type_info($field['type'], 'translate_default');
  284. if ($has_default_value && $storage_has_value && $translate) {
  285. // Compare the default value with the value currently in storage.
  286. if ($instance['default_value'][$delta]['value'] === $items[$delta]['value']) {
  287. $alter_element['#default_value'] = $translate($instance, $items[$delta]['value'], $i18n_langcode);
  288. }
  289. }
  290. }
  291. /**
  292. * Implements hook_field_attach_view_alter().
  293. */
  294. function i18n_field_field_attach_view_alter(&$output, $context) {
  295. foreach (element_children($output) as $field_name) {
  296. $element = &$output[$field_name];
  297. if (!empty($element['#entity_type']) && !empty($element['#field_name']) && !empty($element['#bundle'])) {
  298. $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
  299. // Translate field title if set
  300. if (!empty($instance['label'])) {
  301. $element['#title'] = i18n_field_translate_property($instance, 'label');
  302. }
  303. // Translate field description if set
  304. if (!empty($instance['description'])) {
  305. $element['#description'] = i18n_field_translate_property($instance, 'description');
  306. }
  307. }
  308. }
  309. }
  310. /**
  311. * Implements hook_field_create_field().
  312. */
  313. function i18n_field_field_create_field($field) {
  314. i18n_field_field_update_strings($field);
  315. }
  316. /**
  317. * Implements hook_field_create_instance().
  318. */
  319. function i18n_field_field_create_instance($instance) {
  320. i18n_field_instance_update_strings($instance);
  321. }
  322. /**
  323. * Implements hook_field_delete_instance().
  324. */
  325. function i18n_field_field_delete_instance($instance) {
  326. i18n_string_object_remove('field_instance', $instance);
  327. }
  328. /**
  329. * Implements hook_field_update_instance().
  330. */
  331. function i18n_field_field_update_instance($instance, $prior_instance) {
  332. i18n_field_instance_update_strings($instance);
  333. }
  334. /**
  335. * Implements hook_field_update_field().
  336. */
  337. function i18n_field_field_update_field($field) {
  338. i18n_field_field_update_strings($field);
  339. }
  340. /**
  341. * Update field strings
  342. */
  343. function i18n_field_field_update_strings($field) {
  344. i18n_string_object_update('field', $field);
  345. }
  346. /**
  347. * Update field instance strings
  348. */
  349. function i18n_field_instance_update_strings($instance) {
  350. i18n_string_object_update('field_instance', $instance);
  351. }
  352. /**
  353. * Returns the array of translated allowed values for a list field.
  354. *
  355. * The strings are not safe for output. Keys and values of the array should be
  356. * sanitized through field_filter_xss() before being displayed.
  357. *
  358. * @param $field
  359. * The field definition.
  360. *
  361. * @return
  362. * The array of allowed values. Keys of the array are the raw stored values
  363. * (number or text), values of the array are the display labels.
  364. */
  365. function i18n_field_translate_allowed_values($field, $langcode = NULL) {
  366. if (!empty($field['settings']['allowed_values'])) {
  367. return i18n_string_translate(array('field', $field['field_name'], '#allowed_values'), $field['settings']['allowed_values'], array('langcode' => $langcode, 'sanitize' => FALSE));
  368. }
  369. else {
  370. return array();
  371. }
  372. }
  373. /**
  374. * Translate field default.
  375. */
  376. function i18n_field_translate_default($instance, $value, $langcode = NULL) {
  377. // The default value does not need sanitizing in a text_textfield widget.
  378. $sanitize = !($instance['widget']['type'] == 'text_textfield' && $instance['widget']['module'] == 'text');
  379. return i18n_string_translate(array('field', $instance['field_name'], $instance['bundle'], 'default_value'), $value, array('langcode' => $langcode, 'sanitize' => $sanitize));
  380. }
  381. /**
  382. * Translate field property
  383. */
  384. function i18n_field_translate_property($instance, $property, $langcode = NULL) {
  385. // For performance reasons, we translate the whole instance once, which is cached.
  386. $instance = i18n_string_object_translate('field_instance', $instance, array('langcode' => $langcode));
  387. return $instance[$property];
  388. }
  389. /**
  390. * Get i18n information for translating fields.
  391. *
  392. * @param $type
  393. * Optional field type.
  394. * @param $property
  395. * Optional property to get from field type.
  396. *
  397. * @return
  398. * - The property for the field if $type and $property set.
  399. * - Array of properties for the field type if only $type is set.
  400. * - Array of translation information for all field types.
  401. */
  402. function i18n_field_type_info($type = NULL, $property = NULL) {
  403. $info = &drupal_static(__FUNCTION__);
  404. if (!isset($info)) {
  405. $info = module_invoke_all('i18n_field_info');
  406. drupal_alter('i18n_field_info', $info);
  407. }
  408. if ($property) {
  409. return isset($info[$type]) && isset($info[$type][$property]) ? $info[$type][$property] : NULL;
  410. }
  411. elseif ($type) {
  412. return isset($info[$type]) ? $info[$type] : array();
  413. }
  414. else {
  415. return $info;
  416. }
  417. }
  418. /**
  419. * Implements hook_field_info_alter().
  420. */
  421. function i18n_field_field_info_alter(&$field_info) {
  422. foreach(array_keys($field_info) as $type) {
  423. $field_info[$type]['property_callbacks'][] = 'i18n_field_entity_property_callback';
  424. }
  425. }
  426. /**
  427. * Prime the cache to avoid single db queries for entity fields / properties.
  428. *
  429. * This is mainly uses when large operations are occuring like a flush of the
  430. * entity_property_infos().
  431. */
  432. function i18n_field_prime_caches() {
  433. global $language;
  434. static $cache_primed;
  435. // Fill the cache. This should avoid single db queries when filling the
  436. // properties.
  437. if (empty($cache_primed)) {
  438. $cache_primed = TRUE;
  439. $text_group = i18n_string_textgroup('field');
  440. // Load all strings at once to avoid callbacks for each individual string.
  441. $text_group->load_strings();
  442. $text_group->multiple_translation_search(array('type' => '*', 'objectid' => '*', 'property' => '*'), $language->language);
  443. }
  444. }
  445. /**
  446. * Callback to translate entity property info for a fields.
  447. *
  448. * @see entity_metadata_field_entity_property_info()
  449. * @see entity_metadata_field_default_property_callback()
  450. * @see i18n_field_i18n_object_info_alter()
  451. * @see hook_module_implements_alter()
  452. */
  453. function i18n_field_entity_property_callback(&$info, $entity_type, $field, $instance, $field_type) {
  454. global $language;
  455. // This could create a endless recursion if it's called during rebuilding the
  456. // cache for i18n_object_info(). So if the cache of i18n_object_info isn't
  457. // available yet we assume the worst case, leave the info alone but trigger a
  458. // rebuild of the property when hook_i18n_object_info_alter is invoked. At
  459. // that point the info is available and we can rely on it.
  460. if (!$info = &drupal_static('i18n_object_info')) {
  461. $i18n_field_entity_property_callback_fallback = &drupal_static(__FUNCTION__);
  462. $i18n_field_entity_property_callback_fallback = TRUE;
  463. return;
  464. }
  465. i18n_field_prime_caches();
  466. $name = $field['field_name'];
  467. $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
  468. $property['label'] = i18n_field_translate_property($instance, 'label', $language->language);
  469. }
  470. /**
  471. * Implements hook_i18n_object_info_alter().
  472. */
  473. function i18n_field_i18n_object_info_alter(&$info) {
  474. if (drupal_static('i18n_field_entity_property_callback')) {
  475. if ($info = drupal_static('i18n_object_info')) {
  476. // Clean static and permanent cache of the data and then re-run the property
  477. // building.
  478. // Use a lock to avoid stampeding.
  479. $lock_name = 'i18n_field_entity_property_callback_fallback:' . $GLOBALS['language']->language;
  480. // See if another request is already doing this. If so we bail out here as
  481. // we won't help with anything at the moment.
  482. if (!lock_may_be_available($lock_name)) {
  483. return;
  484. }
  485. if (lock_acquire($lock_name)) {
  486. i18n_field_prime_caches();
  487. // Inject translated properties.
  488. $entity_property_info = entity_get_property_info();
  489. foreach ($entity_property_info as $entity_type => $properties) {
  490. if (isset($properties['bundles'])) {
  491. foreach ($properties['bundles'] as $bundle => $bundle_properties) {
  492. if ($bundle_properties['properties']) {
  493. foreach ($bundle_properties['properties'] as $bundle_property => $bundle_property_info) {
  494. if ($instance = field_info_instance($entity_type, $bundle_property, $bundle)) {
  495. $property = &$entity_property_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$bundle_property];
  496. $property['label'] = i18n_field_translate_property($instance, 'label', $GLOBALS['language']->language);
  497. }
  498. }
  499. }
  500. }
  501. }
  502. }
  503. // Inject into static cache.
  504. $entity_get_property_info = &drupal_static('entity_get_property_info', array());
  505. $entity_get_property_info = $entity_property_info;
  506. // Write permanent cache.
  507. cache_set('entity_property_info:' . $GLOBALS['language']->language, $entity_property_info);
  508. lock_release($lock_name);
  509. }
  510. }
  511. else {
  512. watchdog('i18n_field', 'Unable to run fall-back handling for entity property translation due missing "i18n_object_info" cache', array(), WATCHDOG_WARNING);
  513. }
  514. }
  515. }
  516. /**
  517. * Implements hook_module_implements_alter().
  518. */
  519. function i18n_field_module_implements_alter(&$implementations, $hook) {
  520. if ($hook == 'i18n_object_info_alter') {
  521. // Move our hook implementation to the bottom.
  522. $group = $implementations['i18n_field'];
  523. unset($implementations['i18n_field']);
  524. $implementations['i18n_field'] = $group;
  525. }
  526. }