i18n_string.pages.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <?php
  2. /**
  3. * @file
  4. * Internationalization (i18n) package - translatable strings reusable admin UI.
  5. *
  6. * @author Jose A. Reyero, 2007
  7. */
  8. // Load locale libraries
  9. include_once DRUPAL_ROOT . '/includes/locale.inc';
  10. include_once drupal_get_path('module', 'locale') . '/locale.admin.inc';
  11. /**
  12. * Generate translate page from object.
  13. *
  14. * @param string $object_type
  15. * Obejct type as declared in hook_i18n_object_info().
  16. * @param object $object_value
  17. * Drupal object to translate.
  18. * @param object $language
  19. * Optional language object.
  20. */
  21. function i18n_string_translate_page_object($object_type, $object_value, $language = NULL) {
  22. // For backwards compatibility, ensure parameter is a language object
  23. $language = i18n_language_object($language);
  24. $langcode = $language ? $language->language : NULL;
  25. // Get base keys for all these strings. Object key may be multiple like for blocks (module, delta)
  26. $object = i18n_object($object_type, $object_value);
  27. $strings = $object->get_strings(array('empty' => TRUE));
  28. // If no localizable strings, print message and fail gracefully.
  29. // Possibly this object comes from some other contrib module.
  30. // See http://drupal.org/node/1889878
  31. if (!$strings) {
  32. return t('This object has no strings available for translation.');
  33. }
  34. if (empty($langcode)) {
  35. drupal_set_title(t('Translate !name', array('!name' => i18n_object_info($object_type, 'title'))));
  36. return i18n_string_translate_page_overview($object, $strings);
  37. }
  38. else {
  39. drupal_set_title(t('Translate to !language', array('!language' => i18n_language_name($langcode))));
  40. return drupal_get_form('i18n_string_translate_page_form', $strings, $langcode);
  41. }
  42. }
  43. /**
  44. * Provide a core translation module like overview page for this object.
  45. */
  46. function i18n_string_translate_page_overview($object, $strings) {
  47. $build['i18n_overview'] = drupal_get_form('i18n_string_translate_page_overview_form', $object, $strings);
  48. return $build;
  49. }
  50. /**
  51. * Provide a core translation module like overview page for this object.
  52. */
  53. function i18n_string_translate_page_overview_form($form, &$form_state, $object, $strings) {
  54. // Set the default item key, assume it's the first.
  55. $item_title = reset($strings);
  56. $header = array(
  57. 'language' => t('Language'),
  58. 'title' => t('Title'),
  59. 'status' => t('Status'),
  60. 'operations' => t('Operations')
  61. );
  62. $source_language = variable_get_value('i18n_string_source_language');
  63. $rows = array();
  64. foreach (language_list() as $langcode => $language) {
  65. if ($langcode == $source_language) {
  66. $items = array(
  67. 'language' => check_plain($language->name) . ' ' . t('(source)'),
  68. 'title' => check_plain($item_title->get_string()),
  69. 'status' => t('original'),
  70. 'operations' => l(t('edit'), $object->get_edit_path()),
  71. );
  72. }
  73. else {
  74. // Try to figure out if this item has any of its properties translated.
  75. $translated = FALSE;
  76. foreach ($strings as $i18nstring) {
  77. if ($i18nstring->get_translation($langcode)) {
  78. $translated = TRUE;
  79. break;
  80. }
  81. }
  82. // Translate the item that was requested to be displayed as title.
  83. $items = array(
  84. 'language' => check_plain($language->name),
  85. 'title' => $item_title->format_translation($langcode, array('sanitize default' => TRUE)),
  86. 'status' => $translated ? t('translated') : t('not translated'),
  87. 'operations' => l(t('translate'), $object->get_translate_path($langcode), array('query' => drupal_get_destination())),
  88. );
  89. }
  90. foreach ($items as $key => $markup) {
  91. $rows[$langcode][$key] = $markup;
  92. //$form['#rows'][$langcode][$key]['#markup'] = $markup;
  93. }
  94. }
  95. // Build a form so it can be altered later, with all this information.
  96. $form['object'] = array('#type' => 'value', '#value' => $object);
  97. $form['source_language'] = array('#type' => 'value', '#value' => $source_language);
  98. $form['languages'] = array(
  99. '#header' => $header,
  100. '#rows' => $rows,
  101. '#theme' => 'table',
  102. );
  103. return $form;
  104. }
  105. /**
  106. * Form builder callback for in-place string translation.
  107. *
  108. * @param $strings
  109. * Array of strings indexed by string name (may be indexed by group key too if $groups is present)
  110. * @param $langcode
  111. * Language code to translate to.
  112. * @param $groups
  113. * Optional groups to provide some string grouping. Array with group key and title pairs.
  114. */
  115. function i18n_string_translate_page_form($form, &$form_state, $strings, $langcode, $groups = NULL) {
  116. $form = i18n_string_translate_page_form_base($form, $langcode);
  117. if ($groups) {
  118. // I we've got groups, string list is grouped by group key.
  119. $form['string_groups'] = array('#type' => 'value', '#value' => $strings);
  120. foreach ($groups as $key => $title) {
  121. $form['display'] = array(
  122. '#type' => 'vertical_tabs',
  123. );
  124. $form['strings'][$key] = array(
  125. '#group' => 'display',
  126. '#title' => $title,
  127. '#type' => 'fieldset',
  128. ) + i18n_string_translate_page_form_strings($strings[$key], $langcode);
  129. }
  130. }
  131. else {
  132. // We add a single group with key 'all', but no tabs.
  133. $form['string_groups'] = array('#type' => 'value', '#value' => array('all' => $strings));
  134. $form['strings']['all'] = i18n_string_translate_page_form_strings($strings, $langcode);
  135. }
  136. return $form;
  137. }
  138. /**
  139. * Create base form for string translation
  140. */
  141. function i18n_string_translate_page_form_base($form, $langcode, $redirect = NULL) {
  142. $form['langcode'] = array(
  143. '#type' => 'value',
  144. '#value' => $langcode,
  145. );
  146. $form['submit'] = array(
  147. '#type' => 'submit',
  148. '#value' => t('Save translation'),
  149. '#weight' => 10,
  150. );
  151. if ($redirect) {
  152. $form['#redirect'] = array(
  153. $redirect,
  154. );
  155. }
  156. // Add explicit validate and submit hooks so this can be used from inside any form.
  157. $form['#submit'] = array('i18n_string_translate_page_form_submit');
  158. return $form;
  159. }
  160. /**
  161. * Create field elements for strings
  162. *
  163. * @param i18n_string_object[] $strings
  164. * @param string $langcode
  165. *
  166. * @return array
  167. */
  168. function i18n_string_translate_page_form_strings($strings, $langcode) {
  169. global $user;
  170. $form = array();
  171. foreach ($strings as $item) {
  172. // Check permissions to translate this string, depends on format, etc..
  173. if ($message = $item->check_translate_access()) {
  174. // We'll display a disabled element with the reason it cannot be translated.
  175. $disabled = TRUE;
  176. $description = $message;
  177. }
  178. else {
  179. $disabled = FALSE;
  180. $description = '';
  181. // If we don't have a source and it can be translated, we create it.
  182. if (!$item->get_source()) {
  183. // Enable messages just as a reminder these strings are not being updated properly.
  184. $status = $item->update(array('messages' => TRUE));
  185. if ($status === FALSE || $status === SAVED_DELETED) {
  186. // We don't have a source string so nothing to translate here
  187. $disabled = TRUE;
  188. }
  189. }
  190. }
  191. $default_value = $item->format_translation($langcode, array('langcode' => $langcode, 'sanitize' => FALSE, 'debug' => FALSE));
  192. $available_formats = array_keys(filter_formats($user));
  193. if (!in_array($item->format, $available_formats)) {
  194. $item->format = NULL;
  195. }
  196. $form[$item->get_name()] = array(
  197. '#title' => $item->get_title(),
  198. '#type' => $item->format ? 'text_format' : 'textarea',
  199. '#default_value' => $default_value,
  200. '#format' => $item->format,
  201. // This will trigger i18n_string_pre_render_text_format() to actually
  202. // alter the element.
  203. '#i18n_string_is_translation' => TRUE,
  204. '#disabled' => $disabled,
  205. '#description' => $description,
  206. // If disabled, provide smaller textarea (that can be expanded anyway).
  207. '#rows' => $disabled ? 1 : min(ceil(str_word_count($default_value) / 12), 10),
  208. // Change the parent for disabled strings so we don't get empty values later
  209. '#parents' => array($disabled ? 'disabled_strings': 'strings', $item->get_name()),
  210. );
  211. }
  212. return $form;
  213. }
  214. /**
  215. * Form submission callback for in-place string translation.
  216. */
  217. function i18n_string_translate_page_form_submit($form, &$form_state) {
  218. $count = $success = 0;
  219. foreach ($form_state['values']['strings'] as $name => $value) {
  220. $count++;
  221. list($textgroup, $context) = i18n_string_context(explode(':', $name));
  222. if (is_array($value)) {
  223. if (isset($value['value'])) {
  224. $value = $value['value'];
  225. $form_state['values']['strings'][$name] = $value;
  226. }
  227. else {
  228. form_set_error("strings][$name", t('Unable to get the translated string value.'));
  229. watchdog('locale', 'Unable to get the translated string value, string array is: %string', array('%string' => var_dump($value)), WATCHDOG_WARNING);
  230. }
  231. }
  232. $result = i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value);
  233. $success += ($result ? 1 : 0);
  234. }
  235. if ($success) {
  236. drupal_set_message(format_plural($success, 'A translation was saved successfully.', '@count translations were saved successfully.'));
  237. }
  238. if ($error = $count - $success) {
  239. drupal_set_message(format_plural($error, 'A translation could not be saved.', '@count translations could not be saved.'), 'warning');
  240. }
  241. if (isset($form['#redirect'])) {
  242. $form_state['redirect'] = $form['#redirect'];
  243. }
  244. }
  245. /**
  246. * Menu callback. Saves a string translation coming as POST data.
  247. */
  248. function i18n_string_l10n_client_save_string() {
  249. global $user, $language;
  250. if (user_access('use on-page translation')) {
  251. $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default';
  252. // Other textgroups will be handled by l10n_client module
  253. if (!i18n_string_group_info($textgroup)) {
  254. return l10n_client_save_string();
  255. }
  256. elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['context']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
  257. $name = $textgroup . ':' . $_POST['context'];
  258. if ($i18nstring = i18n_string_get_source($name)) {
  259. // Since this is not a real form, we double check access permissions here too.
  260. if ($error = $i18nstring->check_translate_access()) {
  261. $message = theme('l10n_client_message', array('message' => t('Not saved due to: !reason', array('!reason' => $error)), 'level' => WATCHDOG_WARNING));
  262. }
  263. else {
  264. $result = i18n_string_translation_update($name, $_POST['target'], $language->language, $_POST['source']);
  265. if ($result) {
  266. $message = theme('l10n_client_message', array('message' => t('Translation saved locally for user defined string.'), 'level' => WATCHDOG_INFO));
  267. }
  268. elseif ($result === FALSE) {
  269. $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  270. }
  271. else {
  272. $message = theme('l10n_client_message', array('message' => t('Not saved due to unknown reason.')));
  273. }
  274. }
  275. }
  276. else {
  277. $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
  278. }
  279. }
  280. else {
  281. $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
  282. }
  283. drupal_json_output($message);
  284. exit;
  285. }
  286. }
  287. /**
  288. * User interface for string editing.
  289. */
  290. function i18n_string_locale_translate_edit_form($form, &$form_state, $lid) {
  291. // Fetch source string, if possible.
  292. $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject();
  293. if (!$source) {
  294. drupal_set_message(t('String not found.'), 'error');
  295. drupal_goto('admin/config/regional/translate/translate');
  296. }
  297. // Add original text to the top and some values for form altering.
  298. $form['original'] = array(
  299. '#type' => 'item',
  300. '#title' => t('Original text'),
  301. '#markup' => check_plain(wordwrap($source->source, 0)),
  302. );
  303. if (!empty($source->context)) {
  304. $form['context'] = array(
  305. '#type' => 'item',
  306. '#title' => t('Context'),
  307. '#markup' => check_plain($source->context),
  308. );
  309. }
  310. $form['lid'] = array(
  311. '#type' => 'value',
  312. '#value' => $lid
  313. );
  314. $form['textgroup'] = array(
  315. '#type' => 'value',
  316. '#value' => $source->textgroup,
  317. );
  318. $form['location'] = array(
  319. '#type' => 'value',
  320. '#value' => $source->location
  321. );
  322. // Include default form controls with empty values for all languages.
  323. // This ensures that the languages are always in the same order in forms.
  324. $languages = language_list();
  325. // We don't need the default language value, that value is in $source.
  326. $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language();
  327. unset($languages[($omit)]);
  328. $form['translations'] = array('#tree' => TRUE);
  329. // Approximate the number of rows to use in the default textarea.
  330. $rows = min(ceil(str_word_count($source->source) / 12), 10);
  331. foreach ($languages as $langcode => $language) {
  332. $form['translations'][$langcode] = array(
  333. '#type' => 'textarea',
  334. '#title' => t($language->name),
  335. '#rows' => $rows,
  336. '#default_value' => '',
  337. );
  338. }
  339. // Fetch translations and fill in default values in the form.
  340. $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit));
  341. foreach ($result as $translation) {
  342. $form['translations'][$translation->language]['#default_value'] = $translation->translation;
  343. }
  344. $form['actions'] = array('#type' => 'actions');
  345. $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
  346. // Restrict filter permissions and handle validation and submission for i18n strings.
  347. if (i18n_string_group_info($source->textgroup)) {
  348. if ($i18nstring = i18n_string_get_by_lid($form['lid']['#value'])) {
  349. $form['i18n_string'] = array('#type' => 'value', '#value' => $i18nstring);
  350. if ($message = $i18nstring->check_translate_access()) {
  351. drupal_set_message($message);
  352. $disabled = TRUE;
  353. }
  354. // Add format help anyway, though the form may be disabled.
  355. $form['translations']['format_help']['#markup'] = _i18n_string_translate_format_help($i18nstring->format);
  356. }
  357. else {
  358. drupal_set_message(t('Source string not found.'), 'warning');
  359. $disabled = TRUE;
  360. }
  361. if (!empty($disabled)) {
  362. // Disable all form elements
  363. $form['submit']['#disabled'] = TRUE;
  364. foreach (element_children($form['translations']) as $langcode) {
  365. $form['translations'][$langcode]['#disabled'] = TRUE;
  366. }
  367. }
  368. }
  369. return $form;
  370. }
  371. /**
  372. * Process string editing form validations.
  373. *
  374. * If it is an allowed format, skip default validation, the text will be filtered later
  375. */
  376. function i18n_string_locale_translate_edit_form_validate($form, &$form_state) {
  377. if (empty($form_state['values']['i18n_string'])) {
  378. // If not i18n string use regular locale validation.
  379. $copy_state = $form_state;
  380. locale_translate_edit_form_validate($form, $copy_state);
  381. }
  382. }
  383. /**
  384. * Process string editing form submissions.
  385. *
  386. * Mark translations as current.
  387. */
  388. function i18n_string_locale_translate_edit_form_submit($form, &$form_state) {
  389. // Invoke locale submission.
  390. locale_translate_edit_form_submit($form, $form_state);
  391. $lid = $form_state['values']['lid'];
  392. if ($i18n_string_object = i18n_string_get_by_lid($lid)) {
  393. $i18n_string_object->cache_reset();
  394. }
  395. foreach ($form_state['values']['translations'] as $key => $value) {
  396. if (!empty($value)) {
  397. // An update has been made, so we assume the translation is now current.
  398. db_update('locales_target')
  399. ->fields(array('i18n_status' => I18N_STRING_STATUS_CURRENT))
  400. ->condition('lid', $lid)
  401. ->condition('language', $key)
  402. ->execute();
  403. }
  404. }
  405. }
  406. /**
  407. * Help for text format.
  408. */
  409. function _i18n_string_translate_format_help($format_id) {
  410. $output = '';
  411. if ($format = filter_format_load($format_id)) {
  412. $title = t('Text format: @name', array('@name' => $format->name));
  413. $tips = theme('filter_tips', array('tips' => _filter_tips($format_id, FALSE)));
  414. }
  415. elseif ($format_id == I18N_STRING_FILTER_XSS) {
  416. $title = t('Standard XSS filter.');
  417. $allowed_html = '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>';
  418. $tips[] = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
  419. }
  420. elseif ($format_id == I18N_STRING_FILTER_XSS_ADMIN) {
  421. $title = t('Administration XSS filter.');
  422. $tips[] = t('It will allow most HTML tags but not scripts nor styles.');
  423. }
  424. elseif ($format_id) {
  425. $title = t('Unknown filter: @name', array('@name' => $format_id));
  426. }
  427. if (!empty($title)) {
  428. $output .= '<h5>' . $title . '</h5>';
  429. }
  430. if (!empty($tips)) {
  431. $output .= is_array($tips) ? theme('item_list', array('items' => $tips)) : $tips;
  432. }
  433. return $output;
  434. }