i18n_field.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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 (isset($element['value'])) {
  182. // Number fields. Single-value text fields.
  183. $alter_element = &$element['value'];
  184. }
  185. elseif ($field['type'] == 'entityreference' && isset($element['target_id'])) {
  186. // Entityreference fields using the entityreference_autocomplete widget.
  187. $alter_element = &$element['target_id'];
  188. }
  189. else {
  190. // All other fields.
  191. $alter_element = &$element;
  192. }
  193. // If a subelement has the same title as the parent, translate it instead.
  194. // Allows fields such as email and commerce_price to be translated.
  195. foreach (element_get_visible_children($element) as $key) {
  196. $single_value = ($field['cardinality'] == 1);
  197. $has_title = (isset($element['#title']) && isset($element[$key]['#title']));
  198. if ($single_value && $has_title && $element[$key]['#title'] == $element['#title']) {
  199. $alter_element = &$element[$key];
  200. break;
  201. }
  202. }
  203. // The field language may affect some variables (default) but not others (description will be in current page language)
  204. $i18n_langcode = empty($alter_element['#language']) || $alter_element['#language'] == LANGUAGE_NONE ? $language->language : $alter_element['#language'];
  205. // Translate instance to current page language and set to form_state
  206. // so it will be used for validation messages later.
  207. $instance_current = i18n_string_object_translate('field_instance', $instance);
  208. if (isset($form_state['field'][$instance['field_name']][$langcode]['instance'])) {
  209. $form_state['field'][$instance['field_name']][$langcode]['instance'] = $instance_current;
  210. }
  211. // Translate field title if set and it is the default one.
  212. if (!empty($instance_current['label']) && $instance_current['label'] != $instance['label']) {
  213. if (!empty($alter_element['#title']) && $alter_element['#title'] == check_plain($instance['label'])) {
  214. $alter_element['#title'] = check_plain($instance_current['label']);
  215. }
  216. }
  217. // Translate field description if set and it is the default one.
  218. if (!empty($instance_current['description']) && $instance_current['description'] != $instance['description']) {
  219. if (!empty($alter_element['#description'])) {
  220. // Allow single-value file fields and image fields to have their
  221. // descriptions translated. file_field_widget_form() passes the
  222. // description through theme('file_upload_help'), so i18n_field
  223. // must do the same.
  224. $filefield = in_array($field['type'], array('file', 'image'));
  225. $single_value = ($field['cardinality'] == 1);
  226. $no_default = empty($alter_element['#default_value']['fid']);
  227. if ($filefield && $single_value && $no_default) {
  228. $help_variables = array(
  229. 'description' => field_filter_xss($instance['description']),
  230. 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
  231. );
  232. $original_description = theme('file_upload_help', $help_variables);
  233. if ($alter_element['#description'] == $original_description) {
  234. $help_variables = array(
  235. 'description' => field_filter_xss($instance_current['description']),
  236. 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
  237. );
  238. $alter_element['#description'] = theme('file_upload_help', $help_variables);
  239. }
  240. }
  241. elseif ($alter_element['#description'] == field_filter_xss($instance['description'])) {
  242. $alter_element['#description'] = field_filter_xss($instance_current['description']);
  243. }
  244. }
  245. }
  246. // Translate list options.
  247. $has_options = (!empty($alter_element['#options']) || $field['type'] == 'list_boolean');
  248. $has_allowed_values = !empty($field['settings']['allowed_values']);
  249. $translate = i18n_field_type_info($field['type'], 'translate_options');
  250. if ($has_options && $has_allowed_values && $translate) {
  251. $alter_element['#options'] = $translate($field, $i18n_langcode);
  252. if (isset($alter_element['#properties']) && !empty($alter_element['#properties']['empty_option'])) {
  253. $label = theme('options_none', array('instance' => $instance, 'option' => $alter_element['#properties']['empty_option']));
  254. $alter_element['#options'] = array('_none' => $label) + $alter_element['#options'];
  255. }
  256. // Translate list_boolean fields using the checkboxes widget.
  257. if (!empty($alter_element['#title']) && $field['type'] == 'list_boolean' && !empty($alter_element['#on_value'])) {
  258. $on_value = $alter_element['#on_value'];
  259. $alter_element['#options'];
  260. $alter_element['#title'] = $alter_element['#options'][$on_value];
  261. // For using label instead of "On value".
  262. if ($instance['widget']['settings']['display_label']) {
  263. $alter_element['#title'] = $instance_current['label'];
  264. }
  265. }
  266. }
  267. // Check for more parameters, skip this part if missing.
  268. if (!isset($context['delta']) || !isset($context['items'])) {
  269. return;
  270. }
  271. $delta = $context['delta'];
  272. $items = $context['items'];
  273. // Translate default value.
  274. $has_default_value = (isset($alter_element['#default_value']) && !empty($instance['default_value'][$delta]['value']));
  275. $storage_has_value = !empty($items[$delta]['value']);
  276. $translate = i18n_field_type_info($field['type'], 'translate_default');
  277. if ($has_default_value && $storage_has_value && $translate) {
  278. // Compare the default value with the value currently in storage.
  279. if ($instance['default_value'][$delta]['value'] === $items[$delta]['value']) {
  280. $alter_element['#default_value'] = $translate($instance, $items[$delta]['value'], $i18n_langcode);
  281. }
  282. }
  283. }
  284. /**
  285. * Implements hook_field_attach_view_alter().
  286. */
  287. function i18n_field_field_attach_view_alter(&$output, $context) {
  288. foreach (element_children($output) as $field_name) {
  289. $element = &$output[$field_name];
  290. if (!empty($element['#entity_type']) && !empty($element['#field_name']) && !empty($element['#bundle'])) {
  291. $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
  292. // Translate field title if set
  293. if (!empty($instance['label'])) {
  294. $element['#title'] = i18n_field_translate_property($instance, 'label');
  295. }
  296. // Translate field description if set
  297. if (!empty($instance['description'])) {
  298. $element['#description'] = i18n_field_translate_property($instance, 'description');
  299. }
  300. }
  301. }
  302. }
  303. /**
  304. * Implements hook_field_create_field().
  305. */
  306. function i18n_field_field_create_field($field) {
  307. i18n_field_field_update_strings($field);
  308. }
  309. /**
  310. * Implements hook_field_create_instance().
  311. */
  312. function i18n_field_field_create_instance($instance) {
  313. i18n_field_instance_update_strings($instance);
  314. }
  315. /**
  316. * Implements hook_field_delete_instance().
  317. */
  318. function i18n_field_field_delete_instance($instance) {
  319. i18n_string_object_remove('field_instance', $instance);
  320. }
  321. /**
  322. * Implements hook_field_update_instance().
  323. */
  324. function i18n_field_field_update_instance($instance, $prior_instance) {
  325. i18n_field_instance_update_strings($instance);
  326. }
  327. /**
  328. * Implements hook_field_update_field().
  329. */
  330. function i18n_field_field_update_field($field) {
  331. i18n_field_field_update_strings($field);
  332. }
  333. /**
  334. * Update field strings
  335. */
  336. function i18n_field_field_update_strings($field) {
  337. i18n_string_object_update('field', $field);
  338. }
  339. /**
  340. * Update field instance strings
  341. */
  342. function i18n_field_instance_update_strings($instance) {
  343. i18n_string_object_update('field_instance', $instance);
  344. }
  345. /**
  346. * Returns the array of translated allowed values for a list field.
  347. *
  348. * The strings are not safe for output. Keys and values of the array should be
  349. * sanitized through field_filter_xss() before being displayed.
  350. *
  351. * @param $field
  352. * The field definition.
  353. *
  354. * @return
  355. * The array of allowed values. Keys of the array are the raw stored values
  356. * (number or text), values of the array are the display labels.
  357. */
  358. function i18n_field_translate_allowed_values($field, $langcode = NULL) {
  359. if (!empty($field['settings']['allowed_values'])) {
  360. return i18n_string_translate(array('field', $field['field_name'], '#allowed_values'), $field['settings']['allowed_values'], array('langcode' => $langcode, 'sanitize' => FALSE));
  361. }
  362. else {
  363. return array();
  364. }
  365. }
  366. /**
  367. * Translate field default.
  368. */
  369. function i18n_field_translate_default($instance, $value, $langcode = NULL) {
  370. // The default value does not need sanitizing in a text_textfield widget.
  371. $sanitize = !($instance['widget']['type'] == 'text_textfield' && $instance['widget']['module'] == 'text');
  372. return i18n_string_translate(array('field', $instance['field_name'], $instance['bundle'], 'default_value'), $value, array('langcode' => $langcode, 'sanitize' => $sanitize));
  373. }
  374. /**
  375. * Translate field property
  376. */
  377. function i18n_field_translate_property($instance, $property, $langcode = NULL) {
  378. // For performance reasons, we translate the whole instance once, which is cached.
  379. $instance = i18n_string_object_translate('field_instance', $instance, array('langcode' => $langcode));
  380. return $instance[$property];
  381. }
  382. /**
  383. * Get i18n information for translating fields.
  384. *
  385. * @param $type
  386. * Optional field type.
  387. * @param $property
  388. * Optional property to get from field type.
  389. *
  390. * @return
  391. * - The property for the field if $type and $property set.
  392. * - Array of properties for the field type if only $type is set.
  393. * - Array of translation information for all field types.
  394. */
  395. function i18n_field_type_info($type = NULL, $property = NULL) {
  396. $info = &drupal_static(__FUNCTION__);
  397. if (!isset($info)) {
  398. $info = module_invoke_all('i18n_field_info');
  399. drupal_alter('i18n_field_info', $info);
  400. }
  401. if ($property) {
  402. return isset($info[$type]) && isset($info[$type][$property]) ? $info[$type][$property] : NULL;
  403. }
  404. elseif ($type) {
  405. return isset($info[$type]) ? $info[$type] : array();
  406. }
  407. else {
  408. return $info;
  409. }
  410. }