i18n_string.pages.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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. function i18n_string_translate_page_form_strings($strings, $langcode) {
  164. $formats = filter_formats();
  165. foreach ($strings as $item) {
  166. // We may have a source or not. Load it, our string may get the format from it.
  167. $source = $item->get_source();
  168. $format_id = $source ? $source->format : $item->format;
  169. $description = '';
  170. // Check permissions to translate this string, depends on format, etc..
  171. if ($message = $item->check_translate_access()) {
  172. // We'll display a disabled element with the reason it cannot be translated.
  173. $disabled = TRUE;
  174. $description = $message;
  175. }
  176. else {
  177. $disabled = FALSE;
  178. $description = '';
  179. // If we don't have a source and it can be translated, we create it.
  180. if (!$source) {
  181. // Enable messages just as a reminder these strings are not being updated properly.
  182. $status = $item->update(array('messages' => TRUE));
  183. if ($status === FALSE || $status === SAVED_DELETED) {
  184. // We don't have a source string so nothing to translate here
  185. $disabled = TRUE;
  186. }
  187. else {
  188. $source = $item->get_source();
  189. }
  190. }
  191. }
  192. $default_value = $item->format_translation($langcode, array('langcode' => $langcode, 'sanitize' => FALSE, 'debug' => FALSE));
  193. $form[$item->get_name()] = array(
  194. '#title' => $item->get_title(),
  195. '#type' => 'textarea',
  196. '#default_value' => $default_value,
  197. '#disabled' => $disabled,
  198. '#description' => $description . _i18n_string_translate_format_help($format_id),
  199. //'#i18n_string_format' => $source ? $source->format : 0,
  200. // If disabled, provide smaller textarea (that can be expanded anyway).
  201. '#rows' => $disabled ? 1 : min(ceil(str_word_count($default_value) / 12), 10),
  202. // Change the parent for disabled strings so we don't get empty values later
  203. '#parents' => array($disabled ? 'disabled_strings': 'strings', $item->get_name()),
  204. );
  205. }
  206. return $form;
  207. }
  208. /**
  209. * Form submission callback for in-place string translation.
  210. */
  211. function i18n_string_translate_page_form_submit($form, &$form_state) {
  212. $count = $success = 0;
  213. foreach ($form_state['values']['strings'] as $name => $value) {
  214. $count++;
  215. list($textgroup, $context) = i18n_string_context(explode(':', $name));
  216. $result = i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value);
  217. $success += ($result ? 1 : 0);
  218. }
  219. if ($success) {
  220. drupal_set_message(format_plural($success, 'A translation was saved successfully.', '@count translations were saved successfully.'));
  221. }
  222. if ($error = $count - $success) {
  223. drupal_set_message(format_plural($error, 'A translation could not be saved.', '@count translations could not be saved.'), 'warning');
  224. }
  225. if (isset($form['#redirect'])) {
  226. $form_state['redirect'] = $form['#redirect'];
  227. }
  228. }
  229. /**
  230. * Menu callback. Saves a string translation coming as POST data.
  231. */
  232. function i18n_string_l10n_client_save_string() {
  233. global $user, $language;
  234. if (user_access('use on-page translation')) {
  235. $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default';
  236. // Other textgroups will be handled by l10n_client module
  237. if (!i18n_string_group_info($textgroup)) {
  238. return l10n_client_save_string();
  239. }
  240. elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['context']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
  241. $name = $textgroup . ':' . $_POST['context'];
  242. if ($i18nstring = i18n_string_get_source($name)) {
  243. // Since this is not a real form, we double check access permissions here too.
  244. if ($error = $i18nstring->check_translate_access()) {
  245. $message = theme('l10n_client_message', array('message' => t('Not saved due to: !reason', array('!reason' => $error)), 'level' => WATCHDOG_WARNING));
  246. }
  247. else {
  248. $result = i18n_string_translation_update($name, $_POST['target'], $language->language, $_POST['source']);
  249. if ($result) {
  250. $message = theme('l10n_client_message', array('message' => t('Translation saved locally for user defined string.'), 'level' => WATCHDOG_INFO));
  251. }
  252. elseif ($result === FALSE) {
  253. $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  254. }
  255. else {
  256. $message = theme('l10n_client_message', array('message' => t('Not saved due to unknown reason.')));
  257. }
  258. }
  259. }
  260. else {
  261. $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
  262. }
  263. }
  264. else {
  265. $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
  266. }
  267. drupal_json_output($message);
  268. exit;
  269. }
  270. }
  271. /**
  272. * User interface for string editing.
  273. */
  274. function i18n_string_locale_translate_edit_form($form, &$form_state, $lid) {
  275. // Fetch source string, if possible.
  276. $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject();
  277. if (!$source) {
  278. drupal_set_message(t('String not found.'), 'error');
  279. drupal_goto('admin/config/regional/translate/translate');
  280. }
  281. // Add original text to the top and some values for form altering.
  282. $form['original'] = array(
  283. '#type' => 'item',
  284. '#title' => t('Original text'),
  285. '#markup' => check_plain(wordwrap($source->source, 0)),
  286. );
  287. if (!empty($source->context)) {
  288. $form['context'] = array(
  289. '#type' => 'item',
  290. '#title' => t('Context'),
  291. '#markup' => check_plain($source->context),
  292. );
  293. }
  294. $form['lid'] = array(
  295. '#type' => 'value',
  296. '#value' => $lid
  297. );
  298. $form['textgroup'] = array(
  299. '#type' => 'value',
  300. '#value' => $source->textgroup,
  301. );
  302. $form['location'] = array(
  303. '#type' => 'value',
  304. '#value' => $source->location
  305. );
  306. // Include default form controls with empty values for all languages.
  307. // This ensures that the languages are always in the same order in forms.
  308. $languages = language_list();
  309. // We don't need the default language value, that value is in $source.
  310. $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language();
  311. unset($languages[($omit)]);
  312. $form['translations'] = array('#tree' => TRUE);
  313. // Approximate the number of rows to use in the default textarea.
  314. $rows = min(ceil(str_word_count($source->source) / 12), 10);
  315. foreach ($languages as $langcode => $language) {
  316. $form['translations'][$langcode] = array(
  317. '#type' => 'textarea',
  318. '#title' => t($language->name),
  319. '#rows' => $rows,
  320. '#default_value' => '',
  321. );
  322. }
  323. // Fetch translations and fill in default values in the form.
  324. $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit));
  325. foreach ($result as $translation) {
  326. $form['translations'][$translation->language]['#default_value'] = $translation->translation;
  327. }
  328. $form['actions'] = array('#type' => 'actions');
  329. $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
  330. // Restrict filter permissions and handle validation and submission for i18n strings.
  331. if (i18n_string_group_info($source->textgroup)) {
  332. if ($i18nstring = i18n_string_get_by_lid($form['lid']['#value'])) {
  333. $form['i18n_string'] = array('#type' => 'value', '#value' => $i18nstring);
  334. if ($message = $i18nstring->check_translate_access()) {
  335. drupal_set_message($message);
  336. $disabled = TRUE;
  337. }
  338. // Add format help anyway, though the form may be disabled.
  339. $form['translations']['format_help']['#markup'] = _i18n_string_translate_format_help($i18nstring->format);
  340. }
  341. else {
  342. drupal_set_message(t('Source string not found.'), 'warning');
  343. $disabled = TRUE;
  344. }
  345. if (!empty($disabled)) {
  346. // Disable all form elements
  347. $form['submit']['#disabled'] = TRUE;
  348. foreach (element_children($form['translations']) as $langcode) {
  349. $form['translations'][$langcode]['#disabled'] = TRUE;
  350. }
  351. }
  352. }
  353. return $form;
  354. }
  355. /**
  356. * Process string editing form validations.
  357. *
  358. * If it is an allowed format, skip default validation, the text will be filtered later
  359. */
  360. function i18n_string_locale_translate_edit_form_validate($form, &$form_state) {
  361. if (empty($form_state['values']['i18n_string'])) {
  362. // If not i18n string use regular locale validation.
  363. $copy_state = $form_state;
  364. locale_translate_edit_form_validate($form, $copy_state);
  365. }
  366. }
  367. /**
  368. * Process string editing form submissions.
  369. *
  370. * Mark translations as current.
  371. */
  372. function i18n_string_locale_translate_edit_form_submit($form, &$form_state) {
  373. // Invoke locale submission.
  374. locale_translate_edit_form_submit($form, $form_state);
  375. $lid = $form_state['values']['lid'];
  376. if ($i18n_string_object = i18n_string_get_by_lid($lid)) {
  377. $i18n_string_object->cache_reset();
  378. }
  379. foreach ($form_state['values']['translations'] as $key => $value) {
  380. if (!empty($value)) {
  381. // An update has been made, so we assume the translation is now current.
  382. db_update('locales_target')
  383. ->fields(array('i18n_status' => I18N_STRING_STATUS_CURRENT))
  384. ->condition('lid', $lid)
  385. ->condition('language', $key)
  386. ->execute();
  387. }
  388. }
  389. }
  390. /**
  391. * Help for text format.
  392. */
  393. function _i18n_string_translate_format_help($format_id) {
  394. $output = '';
  395. if ($format = filter_format_load($format_id)) {
  396. $title = t('Text format: @name', array('@name' => $format->name));
  397. $tips = theme('filter_tips', array('tips' => _filter_tips($format_id, FALSE)));
  398. }
  399. elseif ($format_id == I18N_STRING_FILTER_XSS) {
  400. $title = t('Standard XSS filter.');
  401. $allowed_html = '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>';
  402. $tips[] = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
  403. }
  404. elseif ($format_id == I18N_STRING_FILTER_XSS_ADMIN) {
  405. $title = t('Administration XSS filter.');
  406. $tips[] = t('It will allow most HTML tags but not scripts nor styles.');
  407. }
  408. elseif ($format_id) {
  409. $title = t('Unknown filter: @name', array('@name' => $format_id));
  410. }
  411. if (!empty($title)) {
  412. $output .= '<h5>' . $title . '</h5>';
  413. }
  414. if (!empty($tips)) {
  415. $output .= is_array($tips) ? theme('item_list', array('items' => $tips)) : $tips;
  416. }
  417. return $output;
  418. }