i18n_string.module 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. <?php
  2. /**
  3. * @file
  4. * Internationalization (i18n) package - translatable strings.
  5. *
  6. * Object oriented string translation using locale and textgroups. As opposed to core locale strings,
  7. * all strings handled by this module will have a unique id (name), composed by several parts
  8. *
  9. * A string name or string id will have the form 'textgroup:type:objectid:property'. Examples:
  10. *
  11. * - 'profile:field:profile_name:title', will be the title for the profile field 'profile_name'
  12. * - 'taxonomy:term:tid:name', will be the name for the taxonomy term tid
  13. * - 'views:view_name:display_id:footer', footer text
  14. *
  15. * Notes:
  16. * - The object id must be an integer. This is intended for quick indexing of some properties
  17. *
  18. * Some concepts
  19. * - Textgroup. Group the string belongs to, defined by locale hook.
  20. * - Location. Unique id of the string for this textgroup.
  21. * - Name. Unique absolute id of the string: textgroup + location.
  22. * - Context. Object with textgroup, type, objectid, property.
  23. *
  24. * Default language
  25. * - Default language may be English or not. It will be the language set as default.
  26. * Source strings will be stored in default language.
  27. * - In the traditional i18n use case you shouldn't change the default language once defined.
  28. *
  29. * Default language changes
  30. * - You might result in the need to change the default language at a later point.
  31. * - Enabling translation of default language will curcumvent previous limitations.
  32. * - Check i18n_string_translate_langcode() for more details.
  33. *
  34. * The API other modules to translate/update/remove user defined strings consists of
  35. *
  36. * @see i18n_string($name, $string, $langcode)
  37. * @see i18n_string_update($name, $string, $format)
  38. * @see i18n_string_remove($name, $string)
  39. *
  40. * @author Jose A. Reyero, 2007
  41. */
  42. /**
  43. * Translated string is current.
  44. */
  45. define('I18N_STRING_STATUS_CURRENT', 0);
  46. /**
  47. * Translated string needs updating as the source has been edited.
  48. */
  49. define('I18N_STRING_STATUS_UPDATE', 1);
  50. /**
  51. * Source string is obsoleted, cannot be found anymore. To be deleted.
  52. */
  53. define('I18N_STRING_STATUS_DELETE', 2);
  54. /**
  55. * Special string formats/filters: Run through filter_xss
  56. */
  57. define('I18N_STRING_FILTER_XSS', 'FILTER_XSS');
  58. /**
  59. * Special string formats/filters: Run through filter_xss_admin
  60. */
  61. define('I18N_STRING_FILTER_XSS_ADMIN', 'FILTER_XSS_ADMIN');
  62. /**
  63. * Implements hook_help().
  64. */
  65. function i18n_string_help($path, $arg) {
  66. switch ($path) {
  67. case 'admin/help#i18n_string':
  68. $output = '<p>' . t('This module adds support for other modules to translate user defined strings. Depending on which modules you have enabled that use this feature you may see different text groups to translate.') . '<p>';
  69. $output .= '<p>' . t('This works differently to Drupal standard localization system: The strings will be translated from the <a href="@configure-strings">source language</a>, which defaults to the site default language (it may not be English), so changing the default language may cause all these translations to be broken.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
  70. $output .= '<ul>';
  71. $output .= '<li>' . t('To search and translate strings, use the <a href="@translate-interface">translation interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate'))) . '</li>';
  72. $output .= '<li>' . t('If you are missing strings to translate, use the <a href="@refresh-strings">refresh strings</a> page.', array('@refresh-strings' => url('admin/build/translate/refresh'))) . '</li>';
  73. $output .= '</ul>';
  74. $output .= '<p>' . t('Read more on the <em>Internationalization handbook</em>: <a href="http://drupal.org/node/313293">Translating user defined strings</a>.') . '</p>';
  75. return $output;
  76. case 'admin/config/regional/translate/i18n_string':
  77. $output = '<p>' . t('On this page you can refresh and update values for user defined strings.') . '</p>';
  78. $output .= '<ul>';
  79. $output .= '<li>' . t('Use the refresh option when you are missing strings to translate for a given text group. All the strings will be re-created keeping existing translations.') . '</li>';
  80. $output .= '<li>' . t('Use the update option when some of the strings had been previously translated with the localization system, but the translations are not showing up for the configurable strings.') . '</li>';
  81. $output .= '</ul>';
  82. $output .= '<p>' . t('To search and translate strings, use the <a href="@translate-interface">translation interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate'))) . '</p>';
  83. $output .= '<p>' . t('<strong>Important:</strong> To configure which text formats are safe for translation, visit the <a href="@configure-strings">configure strings</a> page before refreshing your strings.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
  84. return $output;
  85. case 'admin/config/regional/language':
  86. $output = '<p>' . t('<strong>Warning</strong>: Changing the default language may have unwanted effects on string translations. Check also the <a href="@configure-strings">source language</a> for translations and read more about <a href="@i18n_string-help">String translation</a>', array('@configure-strings' => url('admin/config/regional/i18n/strings'), '@i18n_string-help' => url('admin/help/i18n_string'))) . '</p>';
  87. return $output;
  88. case 'admin/config/regional/i18n/strings':
  89. $output = '<p>' . t('When translating user defined strings that have a text format associated, translators will be able to edit the text before it is filtered which may be a security risk for some filters. An obvious example is when using the PHP filter but other filters may also be dangerous.') . '</p>';
  90. $output .= '<p>' . t('As a general rule <strong>do not allow any filtered text to be translated unless the translators already have access to that text format</strong>. However if you are doing all your translations through this site\'s translation UI or the Localization client, and never importing translations for other textgroups than <i>default</i>, filter access will be checked for translators on every translation page.') . '</p>';
  91. $output .= '<p>' . t('<strong>Important:</strong> After disallowing some text format, use the <a href="@refresh-strings">refresh strings</a> page so forbidden strings are deleted and not allowed anymore for translators.', array('@refresh-strings' => url('admin/config/regional/translate/i18n_string'))) . '</p>';
  92. return $output;
  93. case 'admin/config/filters':
  94. return '<p>' . t('After updating your text formats do not forget to review the list of formats allowed for string translations on the <a href="@configure-strings">configure translatable strings</a> page.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
  95. }
  96. }
  97. /**
  98. * Implements hook_menu().
  99. */
  100. function i18n_string_menu() {
  101. $items['admin/config/regional/translate/i18n_string'] = array(
  102. 'title' => 'Strings',
  103. 'description' => 'Refresh user defined strings.',
  104. 'weight' => 20,
  105. 'type' => MENU_LOCAL_TASK,
  106. 'page callback' => 'drupal_get_form',
  107. 'page arguments' => array('i18n_string_admin_refresh_form'),
  108. 'file' => 'i18n_string.admin.inc',
  109. 'access arguments' => array('translate interface'),
  110. );
  111. $items['admin/config/regional/i18n/strings'] = array(
  112. 'title' => 'Strings',
  113. 'description' => 'Options for user defined strings.',
  114. 'weight' => 20,
  115. 'type' => MENU_LOCAL_TASK,
  116. 'page callback' => 'drupal_get_form',
  117. 'page arguments' => array('variable_edit_form', array('i18n_string_allowed_formats', 'i18n_string_source_language', 'i18n_string_textgroup_class_[textgroup]')),
  118. 'access arguments' => array('administer site configuration'),
  119. );
  120. // AJAX callback path for strings.
  121. $items['i18n_string/save'] = array(
  122. 'title' => 'Save string',
  123. 'page callback' => 'i18n_string_l10n_client_save_string',
  124. 'access arguments' => array('use on-page translation'),
  125. 'file' => 'i18n_string.pages.inc',
  126. 'type' => MENU_CALLBACK,
  127. );
  128. return $items;
  129. }
  130. /**
  131. * Implements hook_menu_alter().
  132. *
  133. * Take over the locale translation page.
  134. */
  135. function i18n_string_menu_alter(&$items) {
  136. $items['admin/config/regional/translate/edit/%'] = array(
  137. 'title' => 'Edit string',
  138. 'page callback' => 'drupal_get_form',
  139. 'page arguments' => array('i18n_string_locale_translate_edit_form', 5),
  140. 'access arguments' => array('translate interface'),
  141. 'file' => 'i18n_string.pages.inc',
  142. 'file path' => drupal_get_path('module', 'i18n_string'),
  143. );
  144. }
  145. /**
  146. * Implements hook_hook_info().
  147. */
  148. function i18n_string_hook_info() {
  149. $hooks['i18n_string_info'] =
  150. $hooks['i18n_string_list'] =
  151. $hooks['i18n_string_refresh'] =
  152. $hooks['i18n_string_objects'] = array(
  153. 'group' => 'i18n',
  154. );
  155. return $hooks;
  156. }
  157. /**
  158. * Implements hook_locale().
  159. *
  160. * Provide the information from i18n_string groups to locale module
  161. */
  162. function i18n_string_locale($op = 'groups') {
  163. if ($op == 'groups') {
  164. $groups = array();
  165. foreach (i18n_string_group_info() as $name => $info) {
  166. $groups[$name] = $info['title'];
  167. }
  168. return $groups;
  169. }
  170. }
  171. /**
  172. * Implements hook_permission().
  173. */
  174. function i18n_string_permission() {
  175. return array(
  176. 'translate user-defined strings' => array(
  177. 'title' => t('Translate user-defined strings'),
  178. 'description' => t('Translate user-defined strings that are created as part of content or configuration.'),
  179. 'restrict access' => TRUE,
  180. ),
  181. 'translate admin strings' => array(
  182. 'title' => t('Translate admin strings'),
  183. 'description' => t('Translate administrative strings with a very permissive XSS/HTML filter that allows all HTML tags.'),
  184. 'restrict access' => TRUE,
  185. ),
  186. );
  187. }
  188. /**
  189. * Implements hook_modules_enabled().
  190. */
  191. function i18n_string_modules_enabled($modules) {
  192. module_load_include('admin.inc', 'i18n_string');
  193. i18n_string_refresh_enabled_modules($modules);
  194. }
  195. /**
  196. * Implements hook_modules_uninstalled().
  197. */
  198. function i18n_string_modules_uninstalled($modules) {
  199. module_load_include('admin.inc', 'i18n_string');
  200. i18n_string_refresh_uninstalled_modules($modules);
  201. }
  202. /**
  203. * Implements hook_form_FORM_ID_alter()
  204. */
  205. function i18n_string_form_l10n_client_form_alter(&$form, &$form_state) {
  206. $form['#action'] = url('i18n_string/save');
  207. }
  208. /**
  209. * Implements hook_form_FORM_ID_alter()
  210. */
  211. function i18n_string_form_locale_translate_export_po_form_alter(&$form, $form_state) {
  212. $names = locale_language_list('name', TRUE);
  213. if (i18n_string_source_language() != 'en' && array_key_exists('en', $names)) {
  214. $form['langcode']['#options']['en'] = $names['en'];
  215. }
  216. }
  217. /**
  218. * Implements hook_form_FORM_ID_alter()
  219. */
  220. function i18n_string_form_locale_translate_import_form_alter(&$form, $form_state) {
  221. if (i18n_string_source_language() != 'en') {
  222. $names = locale_language_list('name', TRUE);
  223. if (array_key_exists('en', $names)) {
  224. $form['import']['langcode']['#options'][t('Already added languages')]['en'] = $names['en'];
  225. }
  226. else {
  227. $form['import']['langcode']['#options'][t('Languages not yet added')]['en'] = t('English');
  228. }
  229. }
  230. $form['#submit'][] = 'i18n_string_locale_translate_import_form_submit';
  231. }
  232. /**
  233. * Update string data after import form submit
  234. */
  235. function i18n_string_locale_translate_import_form_submit($form, &$form_state) {
  236. if (!drupal_get_messages('error', FALSE) && i18n_string_group_info($form_state['values']['group'])) {
  237. i18n_string_textgroup($form_state['values']['group'])->update_check();
  238. }
  239. }
  240. /**
  241. * Implements hook_element_info_alter().
  242. *
  243. * We need to do this on the element info level as wysiwyg also does so and form
  244. * API (incorrectly) does not merge in the defaults for values that are arrays.
  245. */
  246. function i18n_string_element_info_alter(&$types) {
  247. $types['text_format']['#pre_render'][] = 'i18n_string_pre_render_text_format';
  248. }
  249. /**
  250. * The '#pre_render' function to alter the text format element in a translation.
  251. * The text format for a translation is taken form the original, so the text
  252. * format drop down should be disabled.
  253. *
  254. * @param array $element
  255. * The text_format element which will be rendered.
  256. *
  257. * @return array
  258. * The altered text_format element with a disabled "Text format" select.
  259. */
  260. function i18n_string_pre_render_text_format($element) {
  261. if (!empty($element['#i18n_string_is_translation'])) {
  262. $element['format']['format']['#attributes']['disabled'] = TRUE;
  263. }
  264. return $element;
  265. }
  266. /**
  267. * Check if translation is required for this language code.
  268. *
  269. * Translation is required when default language is different from the given
  270. * language, or when default language translation is explicitly enabled.
  271. *
  272. * No UI is provided to enable translation of default language. On the other
  273. * hand, you can enable/disable translation for a specific language by adding
  274. * the following to your settings.php
  275. *
  276. * @param $langcode
  277. * Optional language code to check. It will default to current request language.
  278. * @code
  279. * // Enable translation of specific language. Language code is 'xx'
  280. * $conf['i18n_string_translate_langcode_xx'] = TRUE;
  281. * // Disable translation of specific language. Language code is 'yy'
  282. * $conf['i18n_string_translate_langcode_yy'] = FALSE;
  283. * @endcode
  284. */
  285. function i18n_string_translate_langcode($langcode = NULL) {
  286. $translate = &drupal_static(__FUNCTION__);
  287. $langcode = isset($langcode) ? $langcode : i18n_langcode();
  288. if (!isset($translate[$langcode])) {
  289. $translate[$langcode] = variable_get('i18n_string_translate_langcode_' . $langcode, i18n_string_source_language() != $langcode);
  290. }
  291. return $translate[$langcode];
  292. }
  293. /**
  294. * Create string class from string name
  295. */
  296. function i18n_string_build($name, $string = NULL) {
  297. list ($group, $context) = i18n_string_context($name);
  298. return i18n_string_textgroup($group)->build_string($context, $string);
  299. }
  300. /**
  301. * Update / create translation source for user defined strings.
  302. *
  303. * @param $name
  304. * Array or string concatenated with ':' that contains textgroup and string context
  305. * @param $string
  306. * Source string in default language. Default language may or may not be English.
  307. * Array of key => string to update multiple strings at once
  308. * @param $options
  309. * Array with additional options:
  310. * - 'format', String format if the string has text format
  311. * - 'messages', Whether to print out status messages
  312. */
  313. function i18n_string_update($name, $string, $options = array()) {
  314. if (is_array($string)) {
  315. return i18n_string_multiple('update', $name, $string, $options);
  316. }
  317. else {
  318. list($textgroup, $context) = i18n_string_context($name);
  319. return i18n_string_textgroup($textgroup)->context_update($context, $string, $options);
  320. }
  321. }
  322. /**
  323. * Update context for strings.
  324. *
  325. * As some string locations depend on configurable values, the field needs sometimes to be updated
  326. * without losing existing translations. I.e:
  327. * - profile fields indexed by field name.
  328. * - content types indexted by low level content type name.
  329. *
  330. * Example:
  331. * 'profile:field:oldfield:*' -> 'profile:field:newfield:*'
  332. */
  333. function i18n_string_update_context($oldname, $newname) {
  334. module_load_install('i18n_string');
  335. return i18n_string_install_update_context($oldname, $newname);
  336. }
  337. /**
  338. * Get textgroup handler.
  339. *
  340. * @return i18n_string_textgroup_default
  341. *
  342. */
  343. function i18n_string_textgroup($textgroup) {
  344. $groups = &drupal_static(__FUNCTION__);
  345. if (!isset($groups[$textgroup])) {
  346. $class = i18n_string_group_info($textgroup, 'class', 'i18n_string_textgroup_default');
  347. $groups[$textgroup] = new $class($textgroup);
  348. }
  349. return $groups[$textgroup];
  350. }
  351. /**
  352. * Check whether a string format is allowed for translation.
  353. */
  354. function i18n_string_allowed_format($format_id = NULL) {
  355. if (!$format_id || $format_id === I18N_STRING_FILTER_XSS || $format_id === I18N_STRING_FILTER_XSS_ADMIN) {
  356. return TRUE;
  357. }
  358. else {
  359. // Check the format still exists an it is in the allowed formats list.
  360. return filter_format_load($format_id) && in_array($format_id, variable_get('i18n_string_allowed_formats', array(filter_fallback_format())), TRUE);
  361. }
  362. }
  363. /**
  364. * Convert string name into textgroup and string context
  365. *
  366. * @param $name
  367. * Array or string concatenated with ':' that contains textgroup and string context
  368. * @param $replace
  369. * Parameter to replace the placeholder ('*') if we are dealing with multiple strings
  370. * Or parameter to append to context if there's no placeholder
  371. *
  372. * @return array
  373. * The first element will be the text group name
  374. * The second one will be an array with string name elements, without textgroup
  375. */
  376. function i18n_string_context($name, $replace = NULL) {
  377. $parts = is_array($name) ? $name : explode(':', $name);
  378. if ($replace) {
  379. $key = array_search('*', $parts);
  380. if ($key !== FALSE) {
  381. $parts[$key] = $replace;
  382. }
  383. elseif (count($parts) < 4) {
  384. array_push($parts, $replace);
  385. }
  386. }
  387. $textgroup = array_shift($parts);
  388. $context = $parts;
  389. return array($textgroup, $context);
  390. }
  391. /**
  392. * Mark form element as localizable
  393. */
  394. function i18n_string_element_mark(&$element) {
  395. $description = '<strong>' . t('This string will be localizable. You can translate it using the <a href="@translate-interface">translate interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate/translate'))) . '</strong>';
  396. if (empty($element['#description'])) {
  397. $element['#description'] = $description;
  398. }
  399. else {
  400. $element['#description'] .= ' ' . $description;
  401. }
  402. }
  403. /**
  404. * Get source string object.
  405. *
  406. * This returns the i18nstring object only when it has a source.
  407. *
  408. * @return i18n_string_object
  409. */
  410. function i18n_string_get_source($name) {
  411. return i18n_string_build($name)->get_source();
  412. }
  413. /**
  414. * Get full string object.
  415. *
  416. * Builds string and loads the source if available.
  417. *
  418. * @return i18n_string_object
  419. */
  420. function i18n_string_get_string($name, $default = NULL) {
  421. $i18nstring = i18n_string_build($name, $default);
  422. $i18nstring->get_source();
  423. return $i18nstring;
  424. }
  425. /**
  426. * Get full string object by lid.
  427. */
  428. function i18n_string_get_by_lid($lid) {
  429. $strings = i18n_string_load_multiple(array('lid' => $lid));
  430. return reset($strings);
  431. }
  432. /**
  433. * Load multiple strings, including string source
  434. *
  435. * @param $conditions
  436. * Array of conditions for i18n_string table.
  437. *
  438. * @return $strings
  439. * List of i18n string objects
  440. */
  441. function i18n_string_load_multiple($conditions) {
  442. // The primary table here will be i18n_string, though we add source too.
  443. $query = db_select('i18n_string', 'i')
  444. ->fields('i');
  445. $query->leftJoin('locales_source', 's', 'i.lid = s.lid');
  446. $query->fields('s', array('source', 'version', 'location'));
  447. // Add text group condition and add conditions to the query
  448. foreach ($conditions as $field => $value) {
  449. $alias = in_array($field, array('source', 'version', 'location')) ? 's' : 'i';
  450. $query->condition($alias . '.' . $field, $value);
  451. }
  452. // this patch is a workaround for core file bug in file
  453. // include/database/prefetch.inc (see: http://drupal.org/node/1567216)
  454. // return $query->execute()->fetchAll(PDO::FETCH_CLASS, 'i18n_string_object');
  455. $stmt = $query->execute();
  456. $stmt->setFetchMode(PDO::FETCH_CLASS, 'i18n_string_object');
  457. return $stmt->fetchAll();
  458. }
  459. /**
  460. * Get textgroup info, from hook_locale('info')
  461. *
  462. * @param $group
  463. * Text group name.
  464. * @param $default
  465. * Default value to return for a property if not set.
  466. */
  467. function i18n_string_group_info($group = NULL, $property = NULL, $default = NULL) {
  468. $info = &drupal_static(__FUNCTION__ , NULL);
  469. if (!isset($info)) {
  470. $info = module_invoke_all('i18n_string_info');
  471. drupal_alter('i18n_string_info', $info);
  472. }
  473. if ($group && $property) {
  474. return isset($info[$group][$property]) ? $info[$group][$property] : $default;
  475. }
  476. elseif ($group) {
  477. return isset($info[$group]) ? $info[$group] : array();
  478. }
  479. else {
  480. return $info;
  481. }
  482. }
  483. /**
  484. * Implements hook_i18n_string_info_alter().
  485. *
  486. * Set determined classes to use for the text group.
  487. */
  488. function i18n_string_i18n_string_info_alter(&$info) {
  489. foreach (array_keys($info) as $name) {
  490. // If class is not defined. Classes from other modules, fixed classes and etc.
  491. if (!isset($info[$name]['class'])) {
  492. $info[$name]['class'] = variable_get('i18n_string_textgroup_class_' . $name, 'i18n_string_textgroup_default');
  493. }
  494. }
  495. }
  496. /**
  497. * Translate / update multiple strings
  498. *
  499. * @param $strings
  500. * Array of name => string pairs
  501. */
  502. function i18n_string_multiple($operation, $name, $strings, $options = array()) {
  503. $result = array();
  504. // Strings may be an array of properties, we need to shift it
  505. if ($operation == 'remove') {
  506. $strings = array_flip($strings);
  507. }
  508. foreach ($strings as $key => $string) {
  509. list($textgroup, $context) = i18n_string_context($name, $key);
  510. array_unshift($context, $textgroup);
  511. $result[$key] = call_user_func('i18n_string_' . $operation, $context, $string, $options);
  512. }
  513. return $result;
  514. }
  515. /**
  516. * @ingroup i18napi
  517. * @{
  518. */
  519. /**
  520. * Get translation for user defined string.
  521. *
  522. * This function is intended to return translations for plain strings that have NO text format
  523. *
  524. * @param array|string name
  525. * Array or string concatenated with ':' that contains textgroup and string context
  526. * @param array|string $string
  527. * A string in the default language, a string wth format (array with keys
  528. * value and format),or an array of strings (without format) to be translated.
  529. * @param array $options
  530. * An associative array of additional options, with the following keys:
  531. * - 'langcode' (defaults to the current language) The language code to translate to a language other than what is used to display the page.
  532. * - 'filter' Filtering callback to apply to the translated string only
  533. * - 'format' Input format to apply to the translated string only
  534. * - 'callback' Callback to apply to the result (both to translated or untranslated string
  535. * - 'sanitize' Whether to filter the translation applying the text format if any, default is TRUE
  536. * - 'sanitize default' Whether to filter the default value if no translation found, default is FALSE
  537. *
  538. * @return string
  539. */
  540. function i18n_string_translate($name, $string, $options = array()) {
  541. if (is_array($string) && isset($string['value'])) {
  542. $string = $string['value'];
  543. }
  544. if (is_array($string)) {
  545. return i18n_string_translate_list($name, $string, $options);
  546. }
  547. else {
  548. $options['langcode'] = $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  549. if (i18n_string_translate_langcode($langcode)) {
  550. list($textgroup, $context) = i18n_string_context($name);
  551. $translation = i18n_string_textgroup($textgroup)->context_translate($context, $string, $options);
  552. // Add for l10n client if available, we pass translation object that contains the format
  553. i18n_string_l10n_client_add($translation, $langcode);
  554. return $translation->format_translation($langcode, $options);
  555. }
  556. else {
  557. // If we don't want to translate to this language, format and return
  558. $options['sanitize'] = !empty($options['sanitize default']);
  559. return i18n_string_format($string, $options);
  560. }
  561. }
  562. }
  563. /**
  564. * Check user access to translate a specific string.
  565. *
  566. * If the string has a format the user is not allowed to edit, it will return FALSE
  567. *
  568. * @param $string_format;
  569. * String object or $format_id
  570. */
  571. function i18n_string_translate_access($string_format, $account = NULL) {
  572. $format_id = is_object($string_format) ? i18n_object_field($string_format, 'format') : $string_format;
  573. return user_access('translate interface', $account) &&
  574. (empty($format_id) || i18n_string_allowed_format($format_id) && ($format = filter_format_load($format_id)) && filter_access($format, $account));
  575. }
  576. /**
  577. * Check whether there is any problem for the user to translate a specific string.
  578. *
  579. * Here we assume the user has 'translate interface' access that should have
  580. * been checked for the page. Possible reasons a user cannot translate a string:
  581. *
  582. * @param $i18nstring
  583. * String object.
  584. * @param $account
  585. * Optional user account, defaults to current user.
  586. *
  587. * @return
  588. * None or empty string if the user has access to translate the string.
  589. * Message if the user cannot translate that string.
  590. */
  591. function i18n_string_translate_check_string($i18nstring, $account = NULL) {
  592. // Check block translation permissions.
  593. if ($i18nstring->textgroup == 'blocks') {
  594. if (!user_access('translate interface', $account) && !user_access('translate blocks', $account)) {
  595. return t('This is a user-defined string within a block. You are not allowed to translate blocks.');
  596. }
  597. }
  598. elseif (!user_access('translate interface', $account) || !user_access('translate user-defined strings', $account)) {
  599. return t('This is a user-defined string. You are not allowed to translate these strings.');
  600. }
  601. if (!empty($i18nstring->format)) {
  602. if (!i18n_string_allowed_format($i18nstring->format)) {
  603. $format = filter_format_load($i18nstring->format);
  604. return t('This string uses the %name text format. Strings with this format are not allowed for translation.', array('%name' => $format->name));
  605. }
  606. elseif ($format = filter_format_load($i18nstring->format)) {
  607. // It is a text format, check user access to that text format.
  608. if (!filter_access($format, $account)) {
  609. return t('This string uses the %name text format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name));
  610. }
  611. }
  612. else {
  613. // This is one of our special formats, I18N_STRING_FILTER_*
  614. if ($i18nstring->format == I18N_STRING_FILTER_XSS_ADMIN && !user_access('translate admin strings', $account)) {
  615. return t('The source string is an administrative string. You are not allowed to translate these strings.');
  616. }
  617. }
  618. }
  619. // No error message, it should be OK to translate.
  620. return '';
  621. }
  622. /**
  623. * Format the resulting translation or the default string applying callbacks
  624. *
  625. * @param $string
  626. * Text string.
  627. * @param $options
  628. * Array of options for string formatting:
  629. * - 'format', text format to apply to the string, defaults to none.
  630. * - 'sanitize', whether to apply the text format, defaults to TRUE.
  631. * - 'cache', text format parameter.
  632. * - 'langcode', text format parameter, defaults to current page language.
  633. * - 'allowed_tags', allowed HTML tags when format is I18N_STRING_FILTER_XSS
  634. */
  635. function i18n_string_format($string, $options = array()) {
  636. $options += array('langcode' => i18n_langcode(), 'format' => FALSE, 'sanitize' => TRUE, 'cache' => FALSE);
  637. // Apply format and callback
  638. if ($string) {
  639. if ($options['sanitize']) {
  640. if ($options['format']) {
  641. // Handle special format values (xss, xss_admin)
  642. switch ($options['format']) {
  643. case I18N_STRING_FILTER_XSS:
  644. $string = !empty($options['allowed_tags']) ? filter_xss($string, $options['allowed_tags']) : filter_xss($string);
  645. break;
  646. case I18N_STRING_FILTER_XSS_ADMIN:
  647. $string = filter_xss_admin($string);
  648. break;
  649. default:
  650. $string = check_markup($string, $options['format'], $options['langcode'], $options['cache']);
  651. }
  652. }
  653. else {
  654. $string = check_plain($string);
  655. }
  656. }
  657. if (isset($options['callback'])) {
  658. $string = call_user_func($options['callback'], $string);
  659. }
  660. }
  661. // Finally, apply prefix and suffix
  662. $options += array('prefix' => '', 'suffix' => '');
  663. return $options['prefix'] . $string . $options['suffix'];
  664. }
  665. /**
  666. * Get filtered translation.
  667. *
  668. * This function is intended to return translations for strings that have a text format
  669. *
  670. * @param $name
  671. * Array or string concatenated with ':' that contains textgroup and string context
  672. * @param $default
  673. * Default string to return if not found, already filtered
  674. * @param $options
  675. * Array with additional options.
  676. */
  677. function i18n_string_text($name, $default, $options = array()) {
  678. $options += array('format' => filter_fallback_format(), 'sanitize' => TRUE);
  679. return i18n_string_translate($name, $default, $options);
  680. }
  681. /**
  682. * Translation for plain string. In case it finds a translation it applies check_plain() to it
  683. *
  684. * @param $name
  685. * Array or string concatenated with ':' that contains textgroup and string context
  686. * @param $default
  687. * Default string to return if not found
  688. * @param $options
  689. * Array with additional options
  690. */
  691. function i18n_string_plain($name, $default, $options = array()) {
  692. $options += array('filter' => 'check_plain');
  693. return i18n_string_translate($name, $default, $options);
  694. }
  695. /**
  696. * Get source language code for translations
  697. */
  698. function i18n_string_source_language() {
  699. return variable_get('i18n_string_source_language', language_default('language'));
  700. }
  701. /**
  702. * Translation for list of options
  703. *
  704. * @param $options
  705. * Array with additional options, some changes
  706. * - 'index' => field that will be mapped to the array key (defaults to 'property')
  707. */
  708. function i18n_string_translate_list($name, $strings, $options = array()) {
  709. $options['langcode'] = $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  710. // If language is default, just return
  711. if (i18n_string_translate_langcode($langcode)) {
  712. // Get textgroup context, preserve placeholder
  713. list($textgroup, $context) = i18n_string_context($name, '*');
  714. $translations = i18n_string_textgroup($textgroup)->multiple_translate($context, $strings, $options);
  715. // Add for l10n client if available, we pass translation object that contains the format
  716. foreach ($translations as $index => $translation) {
  717. i18n_string_l10n_client_add($translation, $langcode);
  718. $strings[$index] = $translation->format_translation($langcode, $options);
  719. }
  720. }
  721. else {
  722. // Format and return
  723. foreach ($strings as $key => $string) {
  724. $strings[$key] = i18n_string_format($string, $options);
  725. }
  726. }
  727. return $strings;
  728. }
  729. /**
  730. * Remove source and translations for user defined string.
  731. *
  732. * Though for most strings the 'name' or 'string id' uniquely identifies that string,
  733. * there are some exceptions (like profile categories) for which we need to use the
  734. * source string itself as a search key.
  735. *
  736. * @param $name
  737. * String name
  738. * @param $string
  739. * Optional source string (string in default language).
  740. * Array of string properties to remove
  741. */
  742. function i18n_string_remove($name, $string = NULL, $options = array()) {
  743. if (is_array($string)) {
  744. return i18n_string_multiple('remove', $name, $string, $options);
  745. }
  746. else {
  747. list($textgroup, $context) = i18n_string_context($name);
  748. return i18n_string_textgroup($textgroup)->context_remove($context, $string, $options);
  749. }
  750. }
  751. /**
  752. * @} End of "ingroup i18napi".
  753. */
  754. /*** l10n client related functions ***/
  755. /**
  756. * Add string to l10n strings if enabled and allowed for this string
  757. *
  758. * @param $context
  759. * String object
  760. */
  761. function i18n_string_l10n_client_add($string, $langcode) {
  762. // If current language add to l10n client list for later on page translation.
  763. // If langcode translation was disabled we are not supossed to reach here.
  764. if (($langcode == i18n_langcode()) && function_exists('l10_client_add_string_to_page') && user_access('translate interface')) {
  765. if (!$string->check_translate_access()) {
  766. $translation = $string->get_translation($langcode);
  767. $source = !empty($string->source) ? $string->source : $string->string;
  768. l10_client_add_string_to_page($source, $translation ? $translation : TRUE, $string->textgroup, $string->context);
  769. }
  770. }
  771. }
  772. /**
  773. * Get information about object string translation
  774. */
  775. function i18n_string_object_info($type = NULL, $property = NULL) {
  776. if ($type) {
  777. if (($info = i18n_object_info($type, 'string translation'))) {
  778. if ($property) {
  779. return isset($info[$property]) ? $info[$property] : NULL;
  780. }
  781. else {
  782. return $info;
  783. }
  784. }
  785. }
  786. else {
  787. $list = array();
  788. foreach (i18n_object_info() as $type => $info) {
  789. if (!empty($info['string translation'])) {
  790. $list[$type] = $info;
  791. }
  792. }
  793. return $list;
  794. }
  795. }
  796. /**
  797. * Implements hook_i18n_object_info_alter().
  798. *
  799. * Set a different default object wrapper for objects that have string translation.
  800. */
  801. function i18n_string_i18n_object_info_alter(&$object_info) {
  802. foreach ($object_info as $type => &$info) {
  803. if (!empty($info['string translation']) && (empty($info['class']) || $info['class'] == 'i18n_object_wrapper')) {
  804. $info['class'] = 'i18n_string_object_wrapper';
  805. }
  806. }
  807. }
  808. /**
  809. * Translate object properties
  810. *
  811. * We clone the object previously so we don't risk translated properties being saved
  812. *
  813. * @param $type
  814. * Object type
  815. * @param $object
  816. * Object or array
  817. */
  818. function i18n_string_object_translate($type, $object, $options = array()) {
  819. $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  820. if (i18n_string_translate_langcode($langcode)) {
  821. // Object properties will be returned without filtering as in the original one.
  822. $options += array('sanitize' => FALSE);
  823. return i18n_object($type, $object)->translate($langcode, $options);
  824. }
  825. else {
  826. return $object;
  827. }
  828. }
  829. /**
  830. * Remove object strings, because object is deleted
  831. *
  832. * @param $type
  833. * Object type
  834. * @param $object
  835. * Object or array
  836. */
  837. function i18n_string_object_remove($type, $object, $options = array()) {
  838. return i18n_object($type, $object)->strings_remove($options);
  839. }
  840. /**
  841. * Update object properties.
  842. *
  843. * @param $type
  844. * Object type
  845. * @param $object
  846. * Object or array
  847. */
  848. function i18n_string_object_update($type, $object, $options = array()) {
  849. return i18n_object($type, $object)->strings_update($options);
  850. }
  851. /**
  852. * Generic translation page for i18n_strings objects.
  853. */
  854. function i18n_string_object_translate_page($object_type, $object_value, $language = NULL) {
  855. module_load_include('inc', 'i18n_string', 'i18n_string.pages');
  856. return i18n_string_translate_page_object($object_type, $object_value, $language);
  857. }
  858. /**
  859. * Preload all strings for this textroup/context.
  860. *
  861. * This is a performance optimization to load all needed strings with a single query.
  862. *
  863. * Examples of valid string name to search are:
  864. * - 'taxonomy:term:*:title'
  865. * This will find all titles for taxonomy terms
  866. * - array('taxonomy', 'term', array(1,2), '*')
  867. * This will find all properties for taxonomy terms 1 and 2
  868. *
  869. * @param $name
  870. * Specially crafted string name, it may take '*' and array parameters for each element.
  871. * @param $langcode
  872. * Language code to search translations. Defaults to current language.
  873. *
  874. * @return array()
  875. * String objects indexed by context.
  876. */
  877. function i18n_string_translation_search($name, $langcode = NULL) {
  878. $langcode = isset($langcode) ? $langcode : i18n_langcode();
  879. list ($textgroup, $context) = i18n_string_context($name);
  880. return i18n_string_textgroup($textgroup)->multiple_translation_search($context, $langcode);
  881. }
  882. /**
  883. * Update / create translation for a certain source.
  884. *
  885. * @param $name
  886. * Array or string concatenated with ':' that contains textgroup and string context
  887. * @param $translation
  888. * Translation string for this language code
  889. * @param $langcode
  890. * The language code to translate to a language other than what is used to display the page.
  891. * @param $source_string
  892. * Optional source string, just in case it needs to be created.
  893. *
  894. * @return mixed
  895. * Source string object if update was successful.
  896. * Null if source string not found.
  897. * FALSE if use doesn't have permission to edit this translation.
  898. */
  899. function i18n_string_translation_update($name, $translation, $langcode, $source_string = NULL) {
  900. if (is_array($translation)) {
  901. return i18n_string_multiple('translation_update', $name, $translation, $langcode);
  902. }
  903. elseif ($source = i18n_string_get_source($name)) {
  904. if ($langcode == i18n_string_source_language()) {
  905. // It's the default language so we should update the string source as well.
  906. i18n_string_update($name, $translation);
  907. }
  908. else {
  909. list ($textgroup, $context) = i18n_string_context($name);
  910. i18n_string_textgroup($textgroup)->update_translation($context, $langcode, $translation);
  911. }
  912. return $source;
  913. }
  914. elseif ($source_string) {
  915. // We don't have a source in the database, so we need to create it, but only if we've got the source too.
  916. // Note this string won't have any format.
  917. i18n_string_update($name, $source_string);
  918. return i18n_string_translation_update($name, $translation, $langcode);
  919. }
  920. else {
  921. return NULL;
  922. }
  923. }
  924. /**
  925. * Count operation results by result value
  926. */
  927. function _i18n_string_result_count($list) {
  928. $result = array();
  929. foreach ($list as $value) {
  930. $key = (string)$value;
  931. $result[$key] = isset($result[$key]) ? $result[$key] +1 : 1;
  932. }
  933. return $result;
  934. }