i18n_string.inc 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524
  1. <?php
  2. /**
  3. * @file
  4. * API for internationalization strings
  5. */
  6. /**
  7. * String object that contains source and translations.
  8. *
  9. * Note all database operations must go through textgroup object so we can switch storage at some point.
  10. */
  11. class i18n_string_object {
  12. // Updated source string
  13. public $string;
  14. // Properties from locale source
  15. public $lid;
  16. public $source;
  17. public $textgroup;
  18. public $location;
  19. public $context;
  20. public $version;
  21. // Properties from i18n_tring
  22. public $type;
  23. public $objectid;
  24. public $property;
  25. public $objectkey;
  26. public $format;
  27. // Properties from metadata
  28. public $title;
  29. // Array of translations to multiple languages
  30. public $translations = array();
  31. // Textgroup object
  32. protected $_textgroup;
  33. /**
  34. * Class constructor
  35. */
  36. public function __construct($data = NULL) {
  37. if ($data) {
  38. $this->set_properties($data);
  39. }
  40. // Attempt to re-build the data from the persistent cache.
  41. $this->rebuild_from_cache($data);
  42. }
  43. /**
  44. * Rebuild the object data based on the persistent cache.
  45. *
  46. * Since the textgroup defines if a string is cacheable or not the caching
  47. * of the string objects happens in the textgroup handler itself.
  48. *
  49. * @see i18n_string_textgroup_cached::__destruct()
  50. */
  51. protected function rebuild_from_cache($data = NULL) {
  52. // Check if we've the required information to repopulate the cache and do so
  53. // if possible.
  54. $meta_data_exist = isset($this->textgroup) && isset($this->type) && isset($this->objectid) && isset($this->property);
  55. if ($meta_data_exist && ($cache = cache_get($this->get_cid())) && !empty($cache->data)) {
  56. // Re-spawn the cached data.
  57. // @TODO do we need a array_diff to ensure we don't overwrite the data
  58. // provided by the $data parameter?
  59. $this->set_properties($cache->data);
  60. }
  61. }
  62. /**
  63. * Reset cache, needed for tests.
  64. */
  65. public function cache_reset() {
  66. $this->translations = array();
  67. // Ensure a possible persistent cache of this object is cleared too.
  68. cache_clear_all($this->get_cid(), 'cache', TRUE);
  69. }
  70. /**
  71. * Returns the caching id for this object.
  72. *
  73. * @return string
  74. * The caching id.
  75. */
  76. public function get_cid() {
  77. return 'i18n:string:obj:' . $this->get_name();
  78. }
  79. /**
  80. * Get message parameters from context and string.
  81. */
  82. public function get_args() {
  83. return array(
  84. '%location' => $this->location,
  85. '%textgroup' => $this->textgroup,
  86. '%string' => ($string = $this->get_string()) ? $string : t('[empty string]'),
  87. );
  88. }
  89. /**
  90. * Set context properties
  91. */
  92. public function set_context($context) {
  93. $parts = is_array($context) ? $context : explode(':', $context);
  94. $this->context = is_array($context) ? implode(':', $context) : $context;
  95. // Location will be the full string name
  96. $this->location = $this->textgroup . ':' . $this->context;
  97. $this->type = array_shift($parts);
  98. $this->objectid = $parts ? array_shift($parts) : '';
  99. $this->objectkey = (int)$this->objectid;
  100. // Remaining elements glued again with ':'
  101. $this->property = $parts ? implode(':', $parts) : '';
  102. // Attempt to re-build the other data from the persistent cache.
  103. $this->rebuild_from_cache();
  104. return $this;
  105. }
  106. /**
  107. * Get string name including textgroup and context
  108. */
  109. public function get_name() {
  110. return $this->textgroup . ':' . $this->type . ':' . $this->objectid . ':' . $this->property;
  111. }
  112. /**
  113. * Get source string
  114. */
  115. public function get_string() {
  116. if (isset($this->string)) {
  117. return $this->string;
  118. }
  119. elseif (isset($this->source)) {
  120. return $this->source;
  121. }
  122. elseif ($this->textgroup()->debug) {
  123. return empty($this->lid) ? t('[Source not found]') : t('[String not found]');
  124. }
  125. else {
  126. return '';
  127. }
  128. }
  129. /**
  130. * Set source string
  131. *
  132. * @param $string
  133. * Plain string or array with 'string', 'format', etc...
  134. */
  135. public function set_string($string) {
  136. if (is_array($string)) {
  137. $this->string = isset($string['string']) ? $string['string'] : NULL;
  138. if (isset($string['format'])) {
  139. $this->format = $string['format'];
  140. }
  141. if (isset($string['title'])) {
  142. $this->title = $string['title'];
  143. }
  144. }
  145. else {
  146. $this->string = $string;
  147. }
  148. return $this;
  149. }
  150. /**
  151. * Get string title.
  152. */
  153. public function get_title() {
  154. return isset($this->title) ? $this->title : t('String');
  155. }
  156. /**
  157. * Get translation to language from string object
  158. */
  159. public function get_translation($langcode) {
  160. if (!isset($this->translations[$langcode])) {
  161. $translation = $this->textgroup()->load_translation($this, $langcode);
  162. if ($translation && isset($translation->translation)) {
  163. $this->set_translation($translation, $langcode);
  164. }
  165. else {
  166. // No source, no translation
  167. $this->translations[$langcode] = FALSE;
  168. }
  169. }
  170. // Which doesn't mean we've got a translation, only that we've got the result cached
  171. return $this->translations[$langcode];
  172. }
  173. /**
  174. * Set translation for language
  175. *
  176. * @param $translation
  177. * Translation object (from database) or string
  178. */
  179. public function set_translation($translation, $langcode = NULL) {
  180. if (is_object($translation)) {
  181. $langcode = $langcode ? $langcode : $translation->language;
  182. $string = isset($translation->translation) ? $translation->translation : FALSE;
  183. $this->set_properties($translation);
  184. }
  185. else {
  186. $string = $translation;
  187. }
  188. $this->translations[$langcode] = $string;
  189. return $this;
  190. }
  191. /**
  192. * Format the resulting translation or the default string applying callbacks
  193. *
  194. * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
  195. */
  196. public function format_translation($langcode, $options = array()) {
  197. $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug);
  198. if ($translation = $this->get_translation($langcode)) {
  199. $string = $translation;
  200. if (isset($options['filter'])) {
  201. $string = call_user_func($options['filter'], $string);
  202. }
  203. }
  204. else {
  205. // Get default source string if no translation.
  206. $string = $this->get_string();
  207. $options['sanitize'] = !empty($options['sanitize default']);
  208. }
  209. if (!empty($this->format)) {
  210. $options += array('format' => $this->format);
  211. }
  212. // Add debug information if enabled
  213. if ($options['debug']) {
  214. $info = array($langcode, $this->textgroup, $this->context);
  215. if (!empty($this->format)) {
  216. $info[] = $this->format;
  217. }
  218. $options += array('suffix' => '');
  219. $options['suffix'] .= ' [' . implode(':', $info) . ']';
  220. }
  221. // Finally, apply options, filters, callback, etc...
  222. return i18n_string_format($string, $options);
  223. }
  224. /**
  225. * Get source string provided a string object.
  226. *
  227. * @return
  228. * String object if source exists.
  229. */
  230. public function get_source() {
  231. // If already searched and not found we don't have a source,
  232. if (isset($this->lid) && !$this->lid) {
  233. return NULL;
  234. }
  235. elseif (!isset($this->lid) || !isset($this->source)) {
  236. // We may have lid from loading a translation but not loaded the source yet.
  237. if ($source = $this->textgroup()->load_source($this)) {
  238. // Set properties but don't override existing ones
  239. $this->set_properties($source, FALSE, FALSE);
  240. if (!isset($this->string)) {
  241. $this->string = $source->source;
  242. }
  243. return $this;
  244. }
  245. else {
  246. $this->lid = FALSE;
  247. return NULL;
  248. }
  249. }
  250. else {
  251. return $this;
  252. }
  253. }
  254. /**
  255. * Set properties from object or array
  256. *
  257. * @param $properties
  258. * Obejct or array of properties
  259. * @param $set_null
  260. * Whether to set null properties too
  261. * @param $override
  262. * Whether to set properties that are already set in this object
  263. */
  264. public function set_properties($properties, $set_null = TRUE, $override = TRUE) {
  265. foreach ((array)$properties as $field => $value) {
  266. if (property_exists($this, $field) && ($set_null || isset($value)) && ($override || !isset($this->$field))) {
  267. $this->$field = $value;
  268. }
  269. }
  270. return $this;
  271. }
  272. /**
  273. * Access textgroup object
  274. */
  275. protected function textgroup() {
  276. if (!isset($this->_textgroup)) {
  277. $this->_textgroup = i18n_string_textgroup($this->textgroup);
  278. }
  279. return $this->_textgroup;
  280. }
  281. /**
  282. * Update this string.
  283. */
  284. public function update($options = array()) {
  285. return $this->textgroup()->string_update($this, $options);
  286. }
  287. /**
  288. * Delete this string.
  289. */
  290. public function remove($options = array()) {
  291. return $this->textgroup()->string_remove($this, $options);
  292. }
  293. /**
  294. * Check whether there is any problem for the user to translate a this string.
  295. *
  296. * @param $account
  297. * Optional user account, defaults to current user.
  298. *
  299. * @return
  300. * None if the user has access to translate the string.
  301. * Error message if the user cannot translate that string.
  302. */
  303. public function check_translate_access($account = NULL) {
  304. return i18n_string_translate_check_string($this, $account);
  305. }
  306. }
  307. /**
  308. * Textgroup handler for i18n_string API
  309. */
  310. class i18n_string_textgroup_default {
  311. // Text group name
  312. public $textgroup;
  313. // Debug flag, set to true to print out more information.
  314. public $debug;
  315. // Cached or preloaded string objects
  316. public $strings = array();
  317. // Multiple translations search map
  318. protected $cache_multiple = array();
  319. /**
  320. * Class constructor.
  321. *
  322. * There are to hidden variables to produce debugging information:
  323. * - 'i18n_string_debug', generic for all text groups.
  324. * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
  325. */
  326. public function __construct($textgroup) {
  327. $this->textgroup = $textgroup;
  328. $this->debug = variable_get('i18n_string_debug', FALSE) || variable_get('i18n_string_debug_' . $textgroup, FALSE);
  329. }
  330. /**
  331. * Build string object
  332. *
  333. * @param $context
  334. * Context array or string
  335. * @param $string string
  336. * Current value for string source
  337. */
  338. public function build_string($context, $string = NULL) {
  339. // First try to locate string on cache
  340. $context = is_array($context) ? implode(':', $context) : $context;
  341. if ($cached = $this->cache_get($context)) {
  342. $i18nstring = $cached;
  343. }
  344. else {
  345. $i18nstring = new i18n_string_object();
  346. $i18nstring->textgroup = $this->textgroup;
  347. $i18nstring->set_context($context);
  348. $this->cache_set($context, $i18nstring);
  349. }
  350. if (isset($string)) {
  351. $i18nstring->set_string($string);
  352. }
  353. return $i18nstring;
  354. }
  355. /**
  356. * Add source string to the locale tables for translation.
  357. *
  358. * It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
  359. * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
  360. *
  361. * This function checks for already existing string without context for this textgroup and updates it accordingly.
  362. * It is intended for backwards compatibility, using already created strings.
  363. *
  364. * @param $i18nstring
  365. * String object
  366. * @param $format
  367. * Text format, for strings that will go through some filter
  368. * @return
  369. * Update status.
  370. */
  371. protected function string_add($i18nstring, $options = array()) {
  372. $options += array('watchdog' => TRUE);
  373. // Default return status if nothing happens
  374. $status = -1;
  375. $source = NULL;
  376. $location = $i18nstring->location;
  377. // The string may not be allowed for translation depending on its format.
  378. if (!$this->string_check($i18nstring, $options)) {
  379. // The format may have changed and it's not allowed now, delete the source string
  380. return $this->string_remove($i18nstring, $options);
  381. }
  382. elseif ($source = $i18nstring->get_source()) {
  383. if ($source->source != $i18nstring->string || $source->location != $location) {
  384. $i18nstring->location = $location;
  385. // String has changed, mark translations for update
  386. $status = $this->save_source($i18nstring);
  387. db_update('locales_target')
  388. ->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))
  389. ->condition('lid', $source->lid)
  390. ->execute();
  391. }
  392. elseif (empty($source->version)) {
  393. // When refreshing strings, we've done version = 0, update it
  394. $this->save_source($i18nstring);
  395. }
  396. }
  397. else {
  398. // We don't have the source object, create it
  399. $status = $this->save_source($i18nstring);
  400. }
  401. // Make sure we have i18n_string part, create or update
  402. // This will also create the source object if doesn't exist
  403. $this->save_string($i18nstring);
  404. if ($options['watchdog']) {
  405. switch ($status) {
  406. case SAVED_UPDATED:
  407. watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
  408. break;
  409. case SAVED_NEW:
  410. watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
  411. break;
  412. }
  413. }
  414. return $status;
  415. }
  416. /**
  417. * Check if string is ok for translation
  418. */
  419. protected static function string_check($i18nstring, $options = array()) {
  420. $options += array('messages' => FALSE, 'watchdog' => TRUE);
  421. if (!empty($i18nstring->format) && !i18n_string_allowed_format($i18nstring->format)) {
  422. // This format is not allowed, so we remove the string, in this case we produce a warning
  423. drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
  424. return FALSE;
  425. }
  426. else {
  427. return TRUE;
  428. }
  429. }
  430. /**
  431. * Filter array of strings
  432. *
  433. * @param array $string_list
  434. * Array of strings to be filtered.
  435. * @param array $filter
  436. * Array of name value conditions.
  437. *
  438. * @return array
  439. * Strings from $string_list that match the filter conditions.
  440. */
  441. protected static function string_filter($string_list, $filter) {
  442. // Remove 'language' and '*' conditions.
  443. if (isset($filter['language'])) {
  444. unset($filter['language']);
  445. }
  446. while ($field = array_search('*', $filter)) {
  447. unset($filter[$field]);
  448. }
  449. foreach ($string_list as $key => $string) {
  450. foreach ($filter as $field => $value) {
  451. if ($string->$field != $value) {
  452. unset($string_list[$key]);
  453. break;
  454. }
  455. }
  456. }
  457. return $string_list;
  458. }
  459. /**
  460. * Build query for i18n_string table
  461. */
  462. protected static function string_query($context, $multiple = FALSE) {
  463. // Search the database using lid if we've got it or textgroup, context otherwise
  464. $query = db_select('i18n_string', 's')->fields('s');
  465. if (!empty($context->lid)) {
  466. $query->condition('s.lid', $context->lid);
  467. }
  468. else {
  469. $query->condition('s.textgroup', $context->textgroup);
  470. if (!$multiple) {
  471. $query->condition('s.context', $context->context);
  472. }
  473. else {
  474. // Query multiple strings
  475. foreach (array('type', 'objectid', 'property') as $field) {
  476. if (!empty($context->$field)) {
  477. $query->condition('s.' . $field, $context->$field);
  478. }
  479. }
  480. }
  481. }
  482. return $query;
  483. }
  484. /**
  485. * Remove string object.
  486. *
  487. * @return
  488. * SAVED_DELETED | FALSE (If the operation failed because no source)
  489. */
  490. public function string_remove($i18nstring, $options = array()) {
  491. $options += array('watchdog' => TRUE, 'messages' => $this->debug);
  492. if ($source = $i18nstring->get_source()) {
  493. db_delete('locales_target')->condition('lid', $source->lid)->execute();
  494. db_delete('i18n_string')->condition('lid', $source->lid)->execute();
  495. db_delete('locales_source')->condition('lid', $source->lid)->execute();
  496. $this->cache_set($source->context, NULL);
  497. if ($options['watchdog']) {
  498. watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
  499. }
  500. if ($options['messages']) {
  501. drupal_set_message(t('Deleted string %location for text group %textgroup: %string', $i18nstring->get_args()));
  502. }
  503. return SAVED_DELETED;
  504. }
  505. else {
  506. if ($options['messages']) {
  507. drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
  508. }
  509. return FALSE;
  510. }
  511. }
  512. /**
  513. * Translate string object
  514. *
  515. * @param $i18nstring
  516. * String object
  517. * @param $options
  518. * Array with aditional options
  519. */
  520. protected function string_translate($i18nstring, $options = array()) {
  521. $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  522. // Search for existing translation (result will be cached in this function call)
  523. $i18nstring->get_translation($langcode);
  524. return $i18nstring;
  525. }
  526. /**
  527. * Update / create / remove string.
  528. *
  529. * @param $name
  530. * String context.
  531. * @pram $string
  532. * New value of string for update/create. May be empty for removing.
  533. * @param $format
  534. * Text format, that must have been checked against allowed formats for translation
  535. * @param $options
  536. * Processing options, the ones used here are:
  537. * - 'watchdog', whether to produce watchdog messages.
  538. * - 'messages', whether to produce user messages.
  539. * - 'check', whether to check string format and then update/delete if not allowed.
  540. * @return status
  541. * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED | FALSE (If the string is to be removed but has no source)
  542. */
  543. public function string_update($i18nstring, $options = array()) {
  544. $options += array('watchdog' => TRUE, 'messages' => $this->debug, 'check' => TRUE);
  545. if ((!$options['check'] || $this->string_check($i18nstring, $options)) && $i18nstring->get_string()) {
  546. // String is ok, has a value so we store it into the database.
  547. $status = $this->string_add($i18nstring, $options);
  548. }
  549. elseif ($i18nstring->get_source()) {
  550. // Just remove it if we already had a source created before.
  551. $status = $this->string_remove($i18nstring, $options);
  552. }
  553. else {
  554. // String didn't pass validation or we have an empty string but was not stored anyway.
  555. $status = FALSE;
  556. }
  557. if ($options['messages']) {
  558. switch ($status) {
  559. case SAVED_UPDATED:
  560. drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
  561. break;
  562. case SAVED_NEW:
  563. drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
  564. break;
  565. }
  566. }
  567. if ($options['watchdog']) {
  568. switch ($status) {
  569. case SAVED_UPDATED:
  570. watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
  571. break;
  572. case SAVED_NEW:
  573. watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
  574. break;
  575. }
  576. }
  577. return $status;
  578. }
  579. /**
  580. * Set string object into cache
  581. */
  582. protected function cache_set($context, $string) {
  583. $this->strings[$context] = $string;
  584. }
  585. /**
  586. * Get translation from cache
  587. */
  588. protected function cache_get($context) {
  589. return isset($this->strings[$context]) ? $this->strings[$context] : NULL;
  590. }
  591. /**
  592. * Reset cache, needed for tests
  593. */
  594. public function cache_reset() {
  595. $this->strings = array();
  596. $this->string_format = array();
  597. // Reset the persistent caches.
  598. cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
  599. // Reset the complete string object cache too.
  600. cache_clear_all('i18n:string:obj:', 'cache', TRUE);
  601. }
  602. /**
  603. * Load multiple strings.
  604. *
  605. * @return array
  606. * List of strings indexed by full string name.
  607. */
  608. public function load_strings($conditions = array()) {
  609. // Add textgroup condition and load all
  610. $conditions['textgroup'] = $this->textgroup;
  611. $list = array();
  612. foreach (i18n_string_load_multiple($conditions) as $string) {
  613. $list[$string->get_name()] = $string;
  614. $this->cache_set($string->context, $string);
  615. }
  616. return $list;
  617. }
  618. /**
  619. * Load string source from db
  620. */
  621. public static function load_source($i18nstring) {
  622. // Search the database using lid if we've got it or textgroup, context otherwise
  623. $query = db_select('locales_source', 's')->fields('s');
  624. $query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
  625. $query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
  626. if (!empty($i18nstring->lid)) {
  627. $query->condition('s.lid', $i18nstring->lid);
  628. }
  629. else {
  630. $query->condition('s.textgroup', $i18nstring->textgroup);
  631. $query->condition('s.context', $i18nstring->context);
  632. }
  633. // Speed up the query, we just need one row
  634. return $query->range(0, 1)->execute()->fetchObject();
  635. }
  636. /**
  637. * Load translation from db
  638. *
  639. * @todo Optimize when we've already got the source string
  640. */
  641. public static function load_translation($i18nstring, $langcode) {
  642. // Search the database using lid if we've got it or textgroup, context otherwise
  643. if (!empty($i18nstring->lid)) {
  644. // We've already got lid, we just need translation data
  645. $query = db_select('locales_target', 't');
  646. $query->condition('t.lid', $i18nstring->lid);
  647. }
  648. else {
  649. // Still don't have lid, load string properties too
  650. $query = db_select('i18n_string', 's')->fields('s');
  651. $query->leftJoin('locales_target', 't', 's.lid = t.lid');
  652. $query->condition('s.textgroup', $i18nstring->textgroup);
  653. $query->condition('s.context', $i18nstring->context);
  654. }
  655. // Add translation fields
  656. $query->fields('t', array('translation', 'i18n_status'));
  657. $query->condition('t.language', $langcode);
  658. // Speed up the query, we just need one row
  659. $query->range(0, 1);
  660. return $query->execute()->fetchObject();
  661. }
  662. /**
  663. * Save / update string object
  664. *
  665. * There seems to be a race condition sometimes so skip errors, #277711
  666. *
  667. * @param $string
  668. * Full string object to be saved
  669. * @param $source
  670. * Source string object
  671. */
  672. protected function save_string($string, $update = FALSE) {
  673. if (!$string->get_source()) {
  674. // Create source string so we get an lid
  675. $this->save_source($string);
  676. }
  677. // Convert objectid to objectkey if it's numeric.
  678. if (!isset($string->objectkey)) {
  679. if (is_numeric($string->objectid)) {
  680. $string->objectkey = (int)$string->objectid;
  681. }
  682. }
  683. // Make sure objectkey is numeric.
  684. if (!is_numeric($string->objectkey)) {
  685. $string->objectkey = 0;
  686. }
  687. if (!isset($string->format)) {
  688. $string->format = '';
  689. }
  690. $status = db_merge('i18n_string')
  691. ->key(array('lid' => $string->lid))
  692. ->fields(array(
  693. 'textgroup' => $string->textgroup,
  694. 'context' => $string->context,
  695. 'objectid' => $string->objectid,
  696. 'type' => $string->type,
  697. 'property' => $string->property,
  698. 'objectindex' => $string->objectkey,
  699. 'format' => $string->format,
  700. ))
  701. ->execute();
  702. return $status;
  703. }
  704. /**
  705. * Save translation to the db
  706. *
  707. * @param $string
  708. * Full string object with translation data (language, translation)
  709. */
  710. protected function save_translation($string, $langcode) {
  711. db_merge('locales_target')
  712. ->key(array('lid' => $string->lid, 'language' => $langcode))
  713. ->fields(array('translation' => $string->get_translation($langcode)))
  714. ->execute();
  715. }
  716. /**
  717. * Save source string (create / update)
  718. */
  719. protected static function save_source($source) {
  720. if (isset($source->string)) {
  721. $source->source = $source->string;
  722. }
  723. if (empty($source->version)) {
  724. $source->version = 1;
  725. }
  726. return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
  727. }
  728. /**
  729. * Remove source and translations for user defined string.
  730. *
  731. * Though for most strings the 'name' or 'string id' uniquely identifies that string,
  732. * there are some exceptions (like profile categories) for which we need to use the
  733. * source string itself as a search key.
  734. *
  735. * @param $context
  736. * Textgroup and location glued with ':'.
  737. * @param $string
  738. * Optional source string (string in default language).
  739. */
  740. public function context_remove($context, $string = NULL, $options = array()) {
  741. $options += array('messages' => $this->debug);
  742. $i18nstring = $this->build_string($context, $string);
  743. $status = $this->string_remove($i18nstring, $options);
  744. return $this;
  745. }
  746. /**
  747. * Translate source string
  748. */
  749. public function context_translate($context, $string, $options = array()) {
  750. $i18nstring = $this->build_string($context, $string);
  751. return $this->string_translate($i18nstring, $options);
  752. }
  753. /**
  754. * Update / create translation source for user defined strings.
  755. *
  756. * @param $name
  757. * Textgroup and location glued with ':'.
  758. * @param $string
  759. * Source string in default language. Default language may or may not be English.
  760. * @param $options
  761. * Array with additional options:
  762. * - 'format', String format if the string has text format.
  763. * - 'messages', Whether to print out status messages.
  764. * - 'check', whether to check string format and then update/delete if not allowed.
  765. */
  766. public function context_update($context, $string, $options = array()) {
  767. $options += array('format' => FALSE, 'messages' => $this->debug, 'watchdog' => TRUE, 'check' => TRUE);
  768. $i18nstring = $this->build_string($context, $string);
  769. $i18nstring->format = $options['format'];
  770. $this->string_update($i18nstring, $options);
  771. return $this;
  772. }
  773. /**
  774. * Build combinations of an array of arrays respecting keys.
  775. *
  776. * Example:
  777. * array(array(a,b), array(1,2)) will translate into
  778. * array(a,1), array(a,2), array(b,1), array(b,2)
  779. */
  780. protected static function multiple_combine($properties) {
  781. $combinations = array();
  782. // Get first key, value. We need to make sure the array pointer is reset.
  783. $value = reset($properties);
  784. $key = key($properties);
  785. array_shift($properties);
  786. $values = is_array($value) ? $value : array($value);
  787. foreach ($values as $value) {
  788. if ($properties) {
  789. foreach (self::multiple_combine($properties) as $merge) {
  790. $combinations[] = array_merge(array($key => $value), $merge);
  791. }
  792. }
  793. else {
  794. $combinations[] = array($key => $value);
  795. }
  796. }
  797. return $combinations;
  798. }
  799. /**
  800. * Get multiple translations with search conditions.
  801. *
  802. * @param $translations
  803. * Array of translation objects as loaded from the db.
  804. * @param $langcode
  805. * Language code, array of language codes or * to search all translations.
  806. *
  807. * @return array
  808. * Array of i18n string objects.
  809. */
  810. protected function multiple_translation_build($translations, $langcode) {
  811. $strings = array();
  812. foreach ($translations as $translation) {
  813. // The string object may be already in list
  814. if (isset($strings[$translation->context])) {
  815. $string = $strings[$translation->context];
  816. }
  817. else {
  818. $string = $this->build_string($translation->context);
  819. $string->set_properties($translation);
  820. $strings[$string->context] = $string;
  821. }
  822. // If this is a translation we set it there too
  823. if ($translation->language && $translation->translation) {
  824. $string->set_translation($translation);
  825. }
  826. elseif ($langcode) {
  827. // This may only happen when we have a source string but not translation.
  828. $string->set_translation(FALSE, $langcode);
  829. }
  830. }
  831. return $strings;
  832. }
  833. /**
  834. * Load multiple translations from db
  835. *
  836. * @todo Optimize when we've already got the source object
  837. *
  838. * @param $conditions
  839. * Array of field values to use as query conditions.
  840. * @param $langcode
  841. * Language code to search.
  842. * @param $index
  843. * Field to use as index for the result.
  844. * @return array
  845. * Array of string objects with translation set.
  846. */
  847. protected function multiple_translation_load($conditions, $langcode) {
  848. $conditions += array(
  849. 'language' => $langcode,
  850. 'textgroup' => $this->textgroup
  851. );
  852. // We may be querying all translations at the same time or just one language.
  853. // The language field needs some special treatment though.
  854. $query = db_select('i18n_string', 's')->fields('s');
  855. $query->leftJoin('locales_target', 't', 's.lid = t.lid');
  856. $query->fields('t', array('translation', 'language', 'i18n_status'));
  857. foreach ($conditions as $field => $value) {
  858. // Single array value, reduce array
  859. if (is_array($value) && count($value) == 1) {
  860. $value = reset($value);
  861. }
  862. if ($value === '*') {
  863. continue;
  864. }
  865. elseif ($field == 'language') {
  866. $query->condition('t.language', $value);
  867. }
  868. else {
  869. $query->condition('s.' . $field, $value);
  870. }
  871. }
  872. return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
  873. }
  874. /**
  875. * Search multiple translations with key combinations.
  876. *
  877. * Each $context field may be a single value, an array of values or '*'.
  878. * Example:
  879. * array('term', array(1,2), '*')
  880. * This will be mapped into the following conditions (provided language code is 'es')
  881. * array('type' => 'term', 'objectid' => array(1,2), 'property' => '*', 'language' => 'es')
  882. * And will result in these combinations to search for
  883. * array('type' => 'term', 'objectid' => 1, 'property' => '*', 'language' => 'es')
  884. * array('type' => 'term', 'objectid' => 2, 'property' => '*', 'language' => 'es')
  885. *
  886. * @param $context array
  887. * Array with String context conditions.
  888. *
  889. * @return
  890. * Array of translation objects indexed by context.
  891. */
  892. public function multiple_translation_search($context, $langcode) {
  893. // First, build conditions and identify the variable field.
  894. $keys = array('type', 'objectid', 'property');
  895. $conditions = array_combine($keys, $context) + array('language' => $langcode);
  896. // Find existing searches in cache, compile remaining ones.
  897. $translations = $search = array();
  898. foreach ($this->multiple_combine($conditions) as $combination) {
  899. $cached = $this->multiple_cache_get($combination);
  900. if (isset($cached)) {
  901. // Cache hit. Merge and remove value from search.
  902. $translations += $cached;
  903. }
  904. else {
  905. // Not in cache, add to search conditions skipping duplicated values.
  906. // As array_merge_recursive() has some bug in PHP 5.2, http://drupal.org/node/1244598
  907. // we use our simplified version here, instead of $search = array_merge_recursive($search, $combination);
  908. foreach ($combination as $key => $value) {
  909. if (!isset($search[$key]) || !in_array($value, $search[$key], TRUE)) {
  910. $search[$key][] = $value;
  911. }
  912. }
  913. }
  914. }
  915. // If we've got any search values left, find translations.
  916. if ($search) {
  917. // Load translations for conditions and set them to the cache
  918. $loaded = $this->multiple_translation_load($search, $langcode);
  919. if ($loaded) {
  920. $translations += $loaded;
  921. }
  922. // Set cache for each of the multiple search keys.
  923. foreach ($this->multiple_combine($search) as $combination) {
  924. $list = $loaded ? $this->string_filter($loaded, $combination) : array();
  925. $this->multiple_cache_set($combination, $list);
  926. }
  927. }
  928. return $translations;
  929. }
  930. /**
  931. * Set multiple cache.
  932. *
  933. * @param $context
  934. * String context with language property at the end.
  935. * @param $strings
  936. * Array of strings (may be empty) to cache.
  937. */
  938. protected function multiple_cache_set($context, $strings) {
  939. $cache_key = implode(':', $context);
  940. $this->cache_multiple[$cache_key] = $strings;
  941. }
  942. /**
  943. * Get strings from multiple cache.
  944. *
  945. * @param $context array
  946. * String context as array with language property at the end.
  947. *
  948. * @return mixed
  949. * Array of strings (may be empty) if we've got a cache hit.
  950. * Null otherwise.
  951. */
  952. protected function multiple_cache_get($context) {
  953. $cache_key = implode(':', $context);
  954. if (isset($this->cache_multiple[$cache_key])) {
  955. return $this->cache_multiple[$cache_key];
  956. }
  957. else {
  958. // Now we try more generic keys. For instance, if we are searching 'term:1:*'
  959. // we may try too 'term:*:*' and filter out the results.
  960. foreach ($context as $key => $value) {
  961. if ($value != '*') {
  962. $try = array_merge($context, array($key => '*'));
  963. $cache_key = implode(':', $try);
  964. if (isset($this->cache_multiple[$cache_key])) {
  965. // As we've found some more generic key, we need to filter using original conditions.
  966. $strings = $this->string_filter($this->cache_multiple[$cache_key], $context);
  967. return $strings;
  968. }
  969. }
  970. }
  971. // If we've reached here, we didn't find any cache match.
  972. return NULL;
  973. }
  974. }
  975. /**
  976. * Translate array of source strings
  977. *
  978. * @param $context
  979. * Context array with placeholders (*)
  980. * @param $strings
  981. * Optional array of source strings indexed by the placeholder property
  982. *
  983. * @return array
  984. * Array of string objects (with translation) indexed by the placeholder field
  985. */
  986. public function multiple_translate($context, $strings = array(), $options = array()) {
  987. // First, build conditions and identify the variable field
  988. $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
  989. $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  990. // If we've got keyed source strings set the array of keys on the placeholder field
  991. // or if not, remove that condition so we search all strings with that keys.
  992. foreach ($search as $field => $value) {
  993. if ($value === '*') {
  994. $property = $field;
  995. if ($strings) {
  996. $search[$field] = array_keys($strings);
  997. }
  998. }
  999. }
  1000. // Now we'll add the language code to conditions and get the translations indexed by the property field
  1001. $result = $this->multiple_translation_search($search, $langcode);
  1002. // Remap translations using property field. If we've got strings it is important that they are in the same order.
  1003. $translations = $strings;
  1004. foreach ($result as $key => $i18nstring) {
  1005. $translations[$i18nstring->$property] = $i18nstring;
  1006. }
  1007. // Set strings as source or create
  1008. foreach ($strings as $key => $source) {
  1009. if (isset($translations[$key]) && is_object($translations[$key])) {
  1010. $translations[$key]->set_string($source);
  1011. }
  1012. else {
  1013. // Not found any string for this property, create it to map in the response
  1014. // But make sure we set this language's translation to FALSE so we don't search again
  1015. $newcontext = $context;
  1016. $newcontext[$property] = $key;
  1017. $translations[$key] = $this->build_string($newcontext)
  1018. ->set_string($source)
  1019. ->set_translation(FALSE, $langcode);
  1020. }
  1021. }
  1022. return $translations;
  1023. }
  1024. /**
  1025. * Update string translation, only if source exists.
  1026. *
  1027. * @param $context
  1028. * String context as array
  1029. * @param $langcode
  1030. * Language code to create the translation for
  1031. * @param $translation
  1032. * String translation for this language
  1033. */
  1034. function update_translation($context, $langcode, $translation) {
  1035. $i18nstring = $this->build_string($context);
  1036. if ($source = $i18nstring->get_source()) {
  1037. $source->set_translation($translation, $langcode);
  1038. $this->save_translation($source, $langcode);
  1039. return $source;
  1040. }
  1041. }
  1042. /**
  1043. * Recheck strings after update
  1044. */
  1045. public function update_check() {
  1046. // Find strings in locales_source that have no data in i18n_string
  1047. $query = db_select('locales_source', 'l')
  1048. ->fields('l')
  1049. ->condition('l.textgroup', $this->textgroup);
  1050. $alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
  1051. $query->isNull('s.lid');
  1052. foreach ($query->execute()->fetchAll() as $string) {
  1053. $i18nstring = $this->build_string($string->context, $string->source);
  1054. $this->save_string($i18nstring);
  1055. }
  1056. }
  1057. }
  1058. /**
  1059. * String object wrapper
  1060. */
  1061. class i18n_string_object_wrapper extends i18n_object_wrapper {
  1062. // Text group object
  1063. protected $textgroup;
  1064. // Properties for translation
  1065. protected $properties;
  1066. /**
  1067. * Get object strings for translation
  1068. *
  1069. * This will return a simple array of string objects, indexed by full string name.
  1070. *
  1071. * @param $options
  1072. * Array with processing options.
  1073. * - 'empty', whether to return empty strings, defaults to FALSE.
  1074. */
  1075. public function get_strings($options = array()) {
  1076. $options += array('empty' => FALSE);
  1077. $strings = array();
  1078. foreach ($this->get_properties() as $textgroup => $textgroup_list) {
  1079. foreach ($textgroup_list as $type => $type_list) {
  1080. foreach ($type_list as $object_id => $object_list) {
  1081. foreach ($object_list as $key => $string) {
  1082. if ($options['empty'] || !empty($string['string'])) {
  1083. // Build string object, that will trigger static caches everywhere.
  1084. $i18nstring = i18n_string_textgroup($textgroup)
  1085. ->build_string(array($type, $object_id, $key))
  1086. ->set_string($string);
  1087. $strings[$i18nstring->get_name()] = $i18nstring;
  1088. }
  1089. }
  1090. }
  1091. }
  1092. }
  1093. return $strings;
  1094. }
  1095. /**
  1096. * Get object translatable properties
  1097. *
  1098. * This will return a big array indexed by textgroup, object type, object id and string key.
  1099. * Each element is an array with string information, and may have these properties:
  1100. * - 'string', the string itself, will be NULL if the object doesn't have that string
  1101. * - 'format', string format when needed
  1102. * - 'title', string readable name
  1103. */
  1104. public function get_properties() {
  1105. if (!isset($this->properties)) {
  1106. $this->properties = $this->build_properties();
  1107. // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
  1108. drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object);
  1109. }
  1110. return $this->properties;
  1111. }
  1112. /**
  1113. * Build properties from object.
  1114. */
  1115. protected function build_properties() {
  1116. list($string_type, $object_id) = $this->get_string_context();
  1117. $object_keys = array(
  1118. $this->get_textgroup(),
  1119. $string_type,
  1120. $object_id,
  1121. );
  1122. $strings = array();
  1123. foreach ($this->get_string_info('properties', array()) as $field => $info) {
  1124. $info = is_array($info) ? $info : array('title' => $info);
  1125. $field_name = isset($info['field']) ? $info['field'] : $field;
  1126. $value = $this->get_field($field_name);
  1127. if (is_array($value) && isset($value['value'])) {
  1128. $format = isset($value['format']) ? $value['format'] : NULL;
  1129. $value = $value['value'];
  1130. }
  1131. else {
  1132. $format = isset($info['format']) ? $this->get_field($info['format']) : NULL;
  1133. }
  1134. $strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
  1135. 'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
  1136. 'title' => $info['title'],
  1137. 'format' => $format,
  1138. 'name' => array_merge($object_keys, array($field)),
  1139. );
  1140. }
  1141. return $strings;
  1142. }
  1143. /**
  1144. * Get string context
  1145. */
  1146. public function get_string_context() {
  1147. return array($this->get_string_info('type'), $this->get_key());
  1148. }
  1149. /**
  1150. * Get translate path for object
  1151. *
  1152. * @param $langcode
  1153. * Language code if we want ti for a specific language
  1154. */
  1155. public function get_translate_path($langcode = NULL) {
  1156. $replacements = array('%i18n_language' => $langcode ? $langcode : '');
  1157. if ($path = $this->get_string_info('translate path')) {
  1158. return $this->path_replace($path, $replacements);
  1159. }
  1160. elseif ($path = $this->get_info('translate tab')) {
  1161. // If we've got a translate tab path, we just add language to it
  1162. return $this->path_replace($path . '/%i18n_language', $replacements);
  1163. }
  1164. }
  1165. /**
  1166. * Translation mode for object
  1167. */
  1168. public function get_translate_mode() {
  1169. return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
  1170. }
  1171. /**
  1172. * Get textgroup name
  1173. */
  1174. public function get_textgroup() {
  1175. return $this->get_string_info('textgroup');
  1176. }
  1177. /**
  1178. * Get textgroup object
  1179. */
  1180. protected function textgroup() {
  1181. if (!isset($this->textgroup)) {
  1182. $this->textgroup = i18n_string_textgroup($this->get_textgroup());
  1183. }
  1184. return $this->textgroup;
  1185. }
  1186. /**
  1187. * Translate object.
  1188. *
  1189. * Translations are cached so it runs only once per language.
  1190. *
  1191. * @return object/array
  1192. * A clone of the object with its properties translated.
  1193. */
  1194. public function translate($langcode, $options = array()) {
  1195. // We may have it already translated. As objects are statically cached, translations are too.
  1196. if (!isset($this->translations[$langcode])) {
  1197. $this->translations[$langcode] = $this->translate_object($langcode, $options);
  1198. }
  1199. return $this->translations[$langcode];
  1200. }
  1201. /**
  1202. * Translate access (localize strings)
  1203. */
  1204. protected function localize_access() {
  1205. // We could check also whether the object has strings to translate:
  1206. // && $this->get_strings(array('empty' => TRUE))
  1207. // However it may be better to display the 'No available strings' message
  1208. // for the user to have a clue of what's going on. See i18n_string_translate_page_object()
  1209. return user_access('translate interface') && user_access('translate user-defined strings');
  1210. }
  1211. /**
  1212. * Translate all properties for object.
  1213. *
  1214. * On top of object strings we search for all textgroup:type:objectid:* properties
  1215. *
  1216. * @param $langcode
  1217. * A clone of the object or array
  1218. */
  1219. protected function translate_object($langcode, $options) {
  1220. // Clone object or array so we don't affect the original one.
  1221. $object = is_object($this->object) ? clone $this->object : $this->object;
  1222. // Get object strings for translatable properties.
  1223. if ($strings = $this->get_strings()) {
  1224. // We preload some of the property translations with a single query.
  1225. if ($context = $this->get_translate_context($langcode, $options)) {
  1226. $found = $this->textgroup()->multiple_translation_search($context, $langcode);
  1227. }
  1228. // Replace all strings in object.
  1229. foreach ($strings as $i18nstring) {
  1230. $this->translate_field($object, $i18nstring, $langcode, $options);
  1231. }
  1232. }
  1233. return $object;
  1234. }
  1235. /**
  1236. * Context to be pre-loaded before translation.
  1237. */
  1238. protected function get_translate_context($langcode, $options) {
  1239. // One-query translation of all textgroup:type:objectid:* properties
  1240. $context = $this->get_string_context();
  1241. $context[] = '*';
  1242. return $context;
  1243. }
  1244. /**
  1245. * Translate object property.
  1246. *
  1247. * Mot often, this is a direct field set, but sometimes fields may have different formats.
  1248. */
  1249. protected function translate_field(&$object, $i18nstring, $langcode, $options) {
  1250. $field_name = $i18nstring->property;
  1251. $translation = $i18nstring->format_translation($langcode, $options);
  1252. if (is_object($object)) {
  1253. $object->$field_name = $translation;
  1254. }
  1255. elseif (is_array($object)) {
  1256. $object[$field_name] = $translation;
  1257. }
  1258. }
  1259. /**
  1260. * Remove all strings for this object.
  1261. */
  1262. public function strings_remove($options = array()) {
  1263. $result = array();
  1264. foreach ($this->load_strings() as $key => $string) {
  1265. $result[$key] = $string->remove($options);
  1266. }
  1267. return _i18n_string_result_count($result);
  1268. }
  1269. /**
  1270. * Update all strings for this object.
  1271. */
  1272. public function strings_update($options = array()) {
  1273. $options += array('empty' => TRUE, 'update' => TRUE);
  1274. $result = array();
  1275. $existing = $this->load_strings();
  1276. // Update object strings
  1277. foreach ($this->get_strings($options) as $key => $string) {
  1278. $result[$key] = $string->update($options);
  1279. unset($existing[$key]);
  1280. }
  1281. // Delete old existing strings.
  1282. foreach ($existing as $key => $string) {
  1283. $result[$key] = $string->remove($options);
  1284. }
  1285. return _i18n_string_result_count($result);
  1286. }
  1287. /**
  1288. * Load all existing strings for this object.
  1289. */
  1290. public function load_strings() {
  1291. list($type, $id) = $this->get_string_context();
  1292. return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
  1293. }
  1294. }
  1295. /**
  1296. * Textgroup handler for i18n_string API which integrated persistent caching.
  1297. */
  1298. class i18n_string_textgroup_cached extends i18n_string_textgroup_default {
  1299. /**
  1300. * Defines the timeout for the persistent caching.
  1301. * @var int
  1302. */
  1303. public $caching_time = CACHE_TEMPORARY;
  1304. /**
  1305. * Extends the existing constructor with a cache handling.
  1306. *
  1307. * @param string $textgroup
  1308. * The name of this textgroup.
  1309. */
  1310. public function __construct($textgroup) {
  1311. parent::__construct($textgroup);
  1312. // Fetch persistent caches, the persistent caches contain only metadata.
  1313. // Those metadata are processed by the related cache_get() methods.
  1314. foreach (array('cache_multiple', 'strings') as $caches_type) {
  1315. if (($cache = cache_get('i18n:string:tgroup:' . $this->textgroup . ':' . $caches_type)) && !empty($cache->data)) {
  1316. $this->{$caches_type} = $cache->data;
  1317. }
  1318. }
  1319. }
  1320. /**
  1321. * Class destructor.
  1322. *
  1323. * Updates the persistent caches for the next usage.
  1324. * This function not only stores the data of the textgroup objects but also
  1325. * of the string objects. That way we ensure that only cacheable string object
  1326. * go into the persistent cache.
  1327. */
  1328. public function __destruct() {
  1329. // Reduce size to cache by removing NULL values.
  1330. $this->strings = array_filter($this->strings);
  1331. $strings_to_cache = array();
  1332. // Store the persistent caches. We just store the metadata the translations
  1333. // are stored by the string object itself. However storing the metadata
  1334. // reduces the number of DB queries executed during runtime.
  1335. $cache_data = array();
  1336. foreach ($this->strings as $context => $i18n_string_object) {
  1337. $cache_data[$context] = $context;
  1338. $strings_to_cache[$context] = $i18n_string_object;
  1339. }
  1340. cache_set('i18n:string:tgroup:' . $this->textgroup . ':strings', $cache_data, 'cache', $this->caching_time);
  1341. $cache_data = array();
  1342. foreach ($this->cache_multiple as $pattern => $strings) {
  1343. foreach ($strings as $context => $i18n_string_object) {
  1344. $cache_data[$pattern][$context] = $context;
  1345. $strings_to_cache[$context] = $i18n_string_object;
  1346. }
  1347. }
  1348. cache_set('i18n:string:tgroup:' . $this->textgroup . ':cache_multiple', $cache_data, 'cache', $this->caching_time);
  1349. // Cache the string objects related to this textgroup.
  1350. // Store only the public visible data into the persistent cache.
  1351. foreach ($strings_to_cache as $i18n_string_object) {
  1352. // If this isn't an object it's an unprocessed cache item and doesn't need
  1353. // to be stored again.
  1354. if (is_object($i18n_string_object)) {
  1355. cache_set($i18n_string_object->get_cid(), get_object_vars($i18n_string_object), 'cache', $this->caching_time);
  1356. }
  1357. }
  1358. }
  1359. /**
  1360. * Reset cache, needed for tests.
  1361. *
  1362. * Takes care of the persistent caches.
  1363. */
  1364. public function cache_reset() {
  1365. // Reset the persistent caches.
  1366. cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
  1367. // Reset the complete string object cache too. This will affect string
  1368. // objects of other textgroups as well.
  1369. cache_clear_all('i18n:string:obj:', 'cache', TRUE);
  1370. return parent::cache_reset();
  1371. }
  1372. /**
  1373. * Get translation from cache.
  1374. *
  1375. * Extends the original handler with persistent caching.
  1376. *
  1377. * @param string $context
  1378. * The context to look out for.
  1379. * @return i18n_string_object|NULL
  1380. * The string object if available or NULL otherwise.
  1381. */
  1382. protected function cache_get($context) {
  1383. if (isset($this->strings[$context])) {
  1384. // If the cache contains a string re-build i18n_string_object.
  1385. if (is_string($this->strings[$context])) {
  1386. $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
  1387. $i8n_string_object->set_context($context);
  1388. $this->strings[$context] = $i8n_string_object;
  1389. }
  1390. // Now run the original handling.
  1391. return parent::cache_get($context);
  1392. }
  1393. return NULL;
  1394. }
  1395. /**
  1396. * Get strings from multiple cache.
  1397. *
  1398. * @param $context array
  1399. * String context as array with language property at the end.
  1400. *
  1401. * @return mixed
  1402. * Array of strings (may be empty) if we've got a cache hit.
  1403. * Null otherwise.
  1404. */
  1405. protected function multiple_cache_get($context) {
  1406. // Ensure the values from the persistent cache are properly re-build.
  1407. $cache_key = implode(':', $context);
  1408. if (isset($this->cache_multiple[$cache_key])) {
  1409. foreach ($this->cache_multiple[$cache_key] as $cached_context) {
  1410. if (is_string($cached_context)) {
  1411. $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
  1412. $i8n_string_object->set_context($cached_context);
  1413. $this->cache_multiple[$cache_key][$cached_context] = $i8n_string_object;
  1414. }
  1415. }
  1416. }
  1417. else {
  1418. // Now we try more generic keys. For instance, if we are searching 'term:1:*'
  1419. // we may try too 'term:*:*' and filter out the results.
  1420. foreach ($context as $key => $value) {
  1421. if ($value != '*') {
  1422. $try = array_merge($context, array($key => '*'));
  1423. $cached_results = $this->multiple_cache_get($try);
  1424. // Now filter the ones that actually match.
  1425. if (!empty($cached_results)) {
  1426. $cached_results = $this->string_filter($cached_results, $context);
  1427. }
  1428. return $cached_results;
  1429. }
  1430. }
  1431. }
  1432. return parent::multiple_cache_get($context);
  1433. }
  1434. public function string_update($i18nstring, $options = array()) {
  1435. // Flush persistent cache.
  1436. cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
  1437. return parent::string_update($i18nstring, $options);
  1438. }
  1439. public function string_remove($i18nstring, $options = array()) {
  1440. // Flush persistent cache.
  1441. cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
  1442. return parent::string_remove($i18nstring, $options);
  1443. }
  1444. }