Projet

Général

Profil

Paste
Télécharger (50,2 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / i18n / i18n_string / i18n_string.inc @ 3115e37e

1
<?php
2
/**
3
 * @file
4
 *   API for internationalization strings
5
 */
6

    
7
/**
8
 * String object that contains source and translations.
9
 *
10
 * Note all database operations must go through textgroup object so we can switch storage at some point.
11
 */
12
class i18n_string_object {
13
  // Updated source string
14
  public $string;
15
  // Properties from locale source
16
  public $lid;
17
  public $source;
18
  public $textgroup;
19
  public $location;
20
  public $context;
21
  public $version;
22
  // Properties from i18n_tring
23
  public $type;
24
  public $objectid;
25
  public $property;
26
  public $objectkey;
27
  public $format;
28
  // Properties from metadata
29
  public $title;
30
  // Array of translations to multiple languages
31
  public $translations = array();
32
  // Textgroup object
33
  protected $_textgroup;
34

    
35
  /**
36
   * Class constructor
37
   */
38
  public function __construct($data = NULL) {
39
    if ($data) {
40
      $this->set_properties($data);
41
    }
42
    // Attempt to re-build the  data from the persistent cache.
43
    $this->rebuild_from_cache($data);
44
  }
45

    
46
  /**
47
   * Rebuild the object data based on the persistent cache.
48
   *
49
   * Since the textgroup defines if a string is cacheable or not the caching
50
   * of the string objects happens in the textgroup handler itself.
51
   *
52
   * @see i18n_string_textgroup_cached::__destruct()
53
   */
54
  protected function rebuild_from_cache($data = NULL) {
55
    // Check if we've the required information to repopulate the cache and do so
56
    // if possible.
57
    $meta_data_exist = isset($this->textgroup) && isset($this->type) && isset($this->objectid) && isset($this->property);
58
    if ($meta_data_exist && ($cache = cache_get($this->get_cid())) && !empty($cache->data)) {
59
      // Re-spawn the cached data.
60
      // @TODO do we need a array_diff to ensure we don't overwrite the data
61
      // provided by the $data parameter?
62
      $this->set_properties($cache->data);
63
    }
64
  }
65

    
66
  /**
67
   * Reset cache, needed for tests.
68
   */
69
  public function cache_reset() {
70
    $this->translations = array();
71
    // Ensure a possible persistent cache of this object is cleared too.
72
    cache_clear_all($this->get_cid(), 'cache', TRUE);
73
  }
74

    
75
  /**
76
   * Returns the caching id for this object.
77
   *
78
   * @return string
79
   *   The caching id.
80
   */
81
  public function get_cid() {
82
    return 'i18n:string:obj:' . $this->get_name();
83
  }
84

    
85
  /**
86
   * Get message parameters from context and string.
87
   */
88
  public function get_args() {
89
    return array(
90
      '%location' => $this->location,
91
      '%textgroup' => $this->textgroup,
92
      '%string' => ($string = $this->get_string()) ? $string : t('[empty string]'),
93
    );
94
  }
95
  /**
96
   * Set context properties
97
   */
98
  public function set_context($context) {
99
    $parts = is_array($context) ? $context : explode(':', $context);
100
    $this->context = is_array($context) ? implode(':', $context) : $context;
101
    // Location will be the full string name
102
    $this->location = $this->textgroup . ':' . $this->context;
103
    $this->type = array_shift($parts);
104
    $this->objectid = $parts ? array_shift($parts) : '';
105
    $this->objectkey = (int)$this->objectid;
106
    // Remaining elements glued again with ':'
107
    $this->property = $parts ? implode(':', $parts) : '';
108

    
109
    // Attempt to re-build the other data from the persistent cache.
110
    $this->rebuild_from_cache();
111

    
112
    return $this;
113
  }
114
  /**
115
   * Get string name including textgroup and context
116
   */
117
  public function get_name() {
118
    return $this->textgroup . ':' . $this->type . ':' . $this->objectid . ':' . $this->property;
119
  }
120
  /**
121
   * Get source string
122
   */
123
  public function get_string() {
124
    if (isset($this->string)) {
125
      return $this->string;
126
    }
127
    elseif (isset($this->source)) {
128
      return $this->source;
129
    }
130
    elseif ($this->textgroup()->debug) {
131
      return empty($this->lid) ? t('[Source not found]') : t('[String not found]');
132
    }
133
    else {
134
      return '';
135
    }
136
  }
137
  /**
138
   * Set source string
139
   *
140
   * @param $string
141
   *   Plain string or array with 'string', 'format', etc...
142
   */
143
  public function set_string($string) {
144
    if (is_array($string)) {
145
      $this->string = isset($string['string']) ? $string['string'] : NULL;
146
      if (isset($string['format'])) {
147
        $this->format = $string['format'];
148
      }
149
      if (isset($string['title'])) {
150
        $this->title = $string['title'];
151
      }
152
    }
153
    else {
154
      $this->string = $string;
155
    }
156

    
157
    return $this;
158
  }
159
  /**
160
   * Get string title.
161
   */
162
  public function get_title() {
163
    return isset($this->title) ? $this->title : t('String');
164
  }
165
  /**
166
   * Get translation to language from string object
167
   */
168
  public function get_translation($langcode) {
169
    if (!isset($this->translations[$langcode])) {
170
      $translation = $this->textgroup()->load_translation($this, $langcode);
171
      if ($translation && isset($translation->translation)) {
172
        $this->set_translation($translation, $langcode);
173
      }
174
      else {
175
        // No source, no translation
176
        $this->translations[$langcode] = FALSE;
177
      }
178
    }
179
    // Which doesn't mean we've got a translation, only that we've got the result cached
180
    return $this->translations[$langcode];
181
  }
182
  /**
183
   * Set translation for language
184
   *
185
   * @param $translation
186
   *   Translation object (from database) or string
187
   */
188
  public function set_translation($translation, $langcode = NULL) {
189
    if (is_object($translation)) {
190
      $langcode = $langcode ? $langcode : $translation->language;
191
      $string = isset($translation->translation) ? $translation->translation : FALSE;
192
      $this->set_properties($translation);
193
    }
194
    else {
195
      $string = $translation;
196
    }
197
    $this->translations[$langcode] = $string;
198
    return $this;
199
  }
200

    
201
  /**
202
   * Format the resulting translation or the default string applying callbacks
203
   *
204
   * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
205
   */
206
  public function format_translation($langcode, $options = array()) {
207
    $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug);
208
    if ($translation = $this->get_translation($langcode)) {
209
      $string = $translation;
210
      if (isset($options['filter'])) {
211
        $string = call_user_func($options['filter'], $string);
212
      }
213
    }
214
    else {
215
      // Get default source string if no translation.
216
      $string = $this->get_string();
217
      $options['sanitize'] = !empty($options['sanitize default']);
218
    }
219
    if (!empty($this->format)) {
220
      $options += array('format' => $this->format);
221
    }
222
    // Add debug information if enabled
223
    if ($options['debug']) {
224
      $info = array($langcode, $this->textgroup, $this->context);
225
      if (!empty($this->format)) {
226
        $info[] = $this->format;
227
      }
228
      $options += array('suffix' => '');
229
      $options['suffix'] .= ' [' . implode(':', $info) . ']';
230
    }
231
    // Finally, apply options, filters, callback, etc...
232
    return i18n_string_format($string, $options);
233
  }
234

    
235
  /**
236
   * Get source string provided a string object.
237
   *
238
   * @return
239
   *   String object if source exists.
240
   */
241
  public function get_source() {
242
    // If already searched and not found we don't have a source,
243
    if (isset($this->lid) && !$this->lid) {
244
      return NULL;
245
    }
246
    elseif (!isset($this->lid) || !isset($this->source)) {
247
      // We may have lid from loading a translation but not loaded the source yet.
248
      if ($source = $this->textgroup()->load_source($this)) {
249
        // Set properties but don't override existing ones
250
        $this->set_properties($source, FALSE, FALSE);
251
        if (!isset($this->string)) {
252
          $this->string = $source->source;
253
        }
254
        return $this;
255
      }
256
      else {
257
        $this->lid = FALSE;
258
        return NULL;
259
      }
260
    }
261
    else {
262
      return $this;
263
    }
264
  }
265

    
266
  /**
267
   * Set properties from object or array
268
   *
269
   * @param $properties
270
   *   Obejct or array of properties
271
   * @param $set_null
272
   *   Whether to set null properties too
273
   * @param $override
274
   *   Whether to set properties that are already set in this object
275
   */
276
  public function set_properties($properties, $set_null = TRUE, $override = TRUE) {
277
    foreach ((array)$properties as $field => $value) {
278
      if (property_exists($this, $field) && ($set_null || isset($value)) && ($override || !isset($this->$field))) {
279
        $this->$field = $value;
280
      }
281
    }
282
    return $this;
283
  }
284
  /**
285
   * Access textgroup object
286
   */
287
  protected function textgroup() {
288
    if (!isset($this->_textgroup)) {
289
      $this->_textgroup = i18n_string_textgroup($this->textgroup);
290
    }
291
    return $this->_textgroup;
292
  }
293
  /**
294
   * Update this string.
295
   */
296
  public function update($options = array()) {
297
    return $this->textgroup()->string_update($this, $options);
298
  }
299
  /**
300
   * Delete this string.
301
   */
302
  public function remove($options = array()) {
303
    return $this->textgroup()->string_remove($this, $options);
304
  }
305
  /**
306
   * Check whether there is any problem for the  user to translate a this string.
307
   *
308
   * @param $account
309
   *   Optional user account, defaults to current user.
310
   *
311
   * @return
312
   *   None if the user has access to translate the string.
313
   *   Error message if the user cannot translate that string.
314
   */
315
  public function check_translate_access($account = NULL) {
316
    return i18n_string_translate_check_string($this, $account);
317
  }
318
}
319

    
320
/**
321
 * Textgroup handler for i18n_string API
322
 */
323
class i18n_string_textgroup_default {
324
  // Text group name
325
  public $textgroup;
326
  // Debug flag, set to true to print out more information.
327
  public $debug;
328
  // Cached or preloaded string objects
329
  public $strings = array();
330
  // Multiple translations search map
331
  protected $cache_multiple = array();
332

    
333
  /**
334
   * Class constructor.
335
   *
336
   * There are to hidden variables to produce debugging information:
337
   * - 'i18n_string_debug', generic for all text groups.
338
   * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
339
   */
340
  public function __construct($textgroup) {
341
    $this->textgroup = $textgroup;
342
    $this->debug = variable_get('i18n_string_debug', FALSE) || variable_get('i18n_string_debug_' . $textgroup, FALSE);
343
  }
344
  /**
345
   * Build string object
346
   *
347
   * @param $context
348
   *   Context array or string
349
   * @param $string string
350
   *   Current value for string source
351
   */
352
  public function build_string($context, $string = NULL) {
353
    // First try to locate string on cache
354
    $context = is_array($context) ? implode(':', $context) : $context;
355
    if ($cached = $this->cache_get($context)) {
356
      $i18nstring = $cached;
357
    }
358
    else {
359
      $i18nstring = new i18n_string_object();
360
      $i18nstring->textgroup = $this->textgroup;
361
      $i18nstring->set_context($context);
362
      $this->cache_set($context, $i18nstring);
363
    }
364
    if (isset($string)) {
365
      $i18nstring->set_string($string);
366
    }
367
    return $i18nstring;
368
  }
369
  /**
370
   * Add source string to the locale tables for translation.
371
   *
372
   * It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
373
   * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
374
   *
375
   * This function checks for already existing string without context for this textgroup and updates it accordingly.
376
   * It is intended for backwards compatibility, using already created strings.
377
   *
378
   * @param $i18nstring
379
   *   String object
380
   * @param $format
381
   *   Text format, for strings that will go through some filter
382
   * @return
383
   *   Update status.
384
   */
385
  protected function string_add($i18nstring, $options = array()) {
386
    $options += array('watchdog' => TRUE);
387
    // Default return status if nothing happens
388
    $status = -1;
389
    $source = NULL;
390
    $location = $i18nstring->location;
391
    // The string may not be allowed for translation depending on its format.
392
    if (!$this->string_check($i18nstring, $options)) {
393
      // The format may have changed and it's not allowed now, delete the source string
394
      return $this->string_remove($i18nstring, $options);
395
    }
396
    elseif ($source = $i18nstring->get_source()) {
397
      if ($source->source != $i18nstring->string || $source->location != $location) {
398
        $i18nstring->location = $location;
399
        // String has changed, mark translations for update
400
        $status = $this->save_source($i18nstring);
401
        db_update('locales_target')
402
          ->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))
403
          ->condition('lid', $source->lid)
404
          ->execute();
405
      }
406
      elseif (empty($source->version)) {
407
        // When refreshing strings, we've done version = 0, update it
408
        $this->save_source($i18nstring);
409
      }
410
    }
411
    else {
412
      // We don't have the source object, create it
413
      $status = $this->save_source($i18nstring);
414
    }
415
    // Make sure we have i18n_string part, create or update
416
    // This will also create the source object if doesn't exist
417
    $this->save_string($i18nstring);
418

    
419
    if ($options['watchdog']) {
420
      switch ($status) {
421
        case SAVED_UPDATED:
422
          watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
423
          break;
424
        case SAVED_NEW:
425
          watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
426
          break;
427
      }
428
    }
429
    return $status;
430
  }
431

    
432
  /**
433
   * Check if string is ok for translation
434
   */
435
  protected static function string_check($i18nstring, $options = array()) {
436
    $options += array('messages' => FALSE, 'watchdog' => TRUE);
437
    if (!empty($i18nstring->format) && !i18n_string_allowed_format($i18nstring->format)) {
438
      // This format is not allowed, so we remove the string, in this case we produce a warning
439
      drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
440
      return FALSE;
441
    }
442
    else {
443
      return TRUE;
444
    }
445
  }
446

    
447
  /**
448
   * Filter array of strings
449
   *
450
   * @param array $string_list
451
   *   Array of strings to be filtered.
452
   * @param array $filter
453
   *   Array of name value conditions.
454
   *
455
   * @return array
456
   *   Strings from $string_list that match the filter conditions.
457
   */
458
  protected static function string_filter($string_list, $filter) {
459
    // Remove 'language' and '*' conditions.
460
    if (isset($filter['language'])) {
461
      unset($filter['language']);
462
    }
463
    while ($field = array_search('*', $filter)) {
464
      unset($filter[$field]);
465
    }
466
    foreach ($string_list as $key => $string) {
467
      foreach ($filter as $field => $value) {
468
        if ($string->$field != $value) {
469
          unset($string_list[$key]);
470
          break;
471
        }
472
      }
473
    }
474
    return $string_list;
475
  }
476

    
477
  /**
478
   * Build query for i18n_string table
479
   */
480
  protected static function string_query($context, $multiple = FALSE) {
481
    // Search the database using lid if we've got it or textgroup, context otherwise
482
    $query = db_select('i18n_string', 's')->fields('s');
483
    if (!empty($context->lid)) {
484
      $query->condition('s.lid', $context->lid);
485
    }
486
    else {
487
      $query->condition('s.textgroup', $context->textgroup);
488
      if (!$multiple) {
489
        $query->condition('s.context', $context->context);
490
      }
491
      else {
492
        // Query multiple strings
493
        foreach (array('type', 'objectid', 'property') as $field) {
494
          if (!empty($context->$field)) {
495
            $query->condition('s.' . $field, $context->$field);
496
          }
497
        }
498
      }
499
    }
500
    return $query;
501
  }
502

    
503
  /**
504
   * Remove string object.
505
   *
506
   * @return
507
   *   SAVED_DELETED | FALSE (If the operation failed because no source)
508
   */
509
  public function string_remove($i18nstring, $options = array()) {
510
    $options += array('watchdog' => TRUE, 'messages' => $this->debug);
511
    if ($source = $i18nstring->get_source()) {
512
      db_delete('locales_target')->condition('lid', $source->lid)->execute();
513
      db_delete('i18n_string')->condition('lid', $source->lid)->execute();
514
      db_delete('locales_source')->condition('lid', $source->lid)->execute();
515
      $this->cache_set($source->context, NULL);
516
      if ($options['watchdog']) {
517
        watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
518
      }
519
      if ($options['messages']) {
520
        drupal_set_message(t('Deleted string %location for text group %textgroup: %string', $i18nstring->get_args()));
521
      }
522
      return SAVED_DELETED;
523
    }
524
    else {
525
      if ($options['messages']) {
526
        drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
527
      }
528
      return FALSE;
529
    }
530
  }
531

    
532
  /**
533
   * Translate string object
534
   *
535
   * @param $i18nstring
536
   *   String object
537
   * @param $options
538
   *   Array with aditional options
539
   */
540
  protected function string_translate($i18nstring, $options = array()) {
541
    $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
542
    // Search for existing translation (result will be cached in this function call)
543
    $i18nstring->get_translation($langcode);
544
    return $i18nstring;
545
  }
546

    
547
  /**
548
   * Update / create / remove string.
549
   *
550
   * @param $name
551
   *   String context.
552
   * @pram $string
553
   *   New value of string for update/create. May be empty for removing.
554
   * @param $format
555
   *   Text format, that must have been checked against allowed formats for translation
556
   * @param $options
557
   *   Processing options, the ones used here are:
558
   *   - 'watchdog', whether to produce watchdog messages.
559
   *   - 'messages', whether to produce user messages.
560
   *   - 'check', whether to check string format and then update/delete if not allowed.
561
   * @return status
562
   *   SAVED_UPDATED | SAVED_NEW | SAVED_DELETED | FALSE (If the string is to be removed but has no source)
563
   */
564
  public function string_update($i18nstring, $options = array()) {
565
    $options += array('watchdog' => TRUE, 'messages' => $this->debug, 'check' => TRUE);
566
    if ((!$options['check'] || $this->string_check($i18nstring, $options)) && $i18nstring->get_string()) {
567
      // String is ok, has a value so we store it into the database.
568
      $status = $this->string_add($i18nstring, $options);
569
    }
570
    elseif ($i18nstring->get_source()) {
571
      // Just remove it if we already had a source created before.
572
      $status = $this->string_remove($i18nstring, $options);
573
    }
574
    else {
575
      // String didn't pass validation or we have an empty string but was not stored anyway.
576
      $status = FALSE;
577
    }
578
    if ($options['messages']) {
579
      switch ($status) {
580
        case SAVED_UPDATED:
581
          drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
582
          break;
583
        case SAVED_NEW:
584
          drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
585
          break;
586
      }
587
    }
588
    if ($options['watchdog']) {
589
      switch ($status) {
590
        case SAVED_UPDATED:
591
          watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
592
          break;
593
        case SAVED_NEW:
594
          watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
595
          break;
596
      }
597
    }
598
    return $status;
599
  }
600

    
601
  /**
602
   * Set string object into cache
603
   */
604
  protected function cache_set($context, $string) {
605
    $this->strings[$context] = $string;
606
  }
607

    
608
  /**
609
   * Get translation from cache
610
   */
611
  protected function cache_get($context) {
612
    return isset($this->strings[$context]) ? $this->strings[$context] : NULL;
613
  }
614

    
615
  /**
616
   * Reset cache, needed for tests
617
   */
618
  public function cache_reset() {
619
    $this->strings = array();
620
    $this->string_format = array();
621

    
622
    // Reset the persistent caches.
623
    cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
624
    // Reset the complete string object cache too.
625
    cache_clear_all('i18n:string:obj:', 'cache', TRUE);
626
  }
627

    
628
  /**
629
   * Load multiple strings.
630
   *
631
   * @return array
632
   *   List of strings indexed by full string name.
633
   */
634
  public function load_strings($conditions = array()) {
635
    // Add textgroup condition and load all
636
    $conditions['textgroup'] = $this->textgroup;
637
    $list = array();
638
    foreach (i18n_string_load_multiple($conditions) as $string) {
639
      $list[$string->get_name()] = $string;
640
      $this->cache_set($string->context, $string);
641
    }
642
    return $list;
643
  }
644

    
645
  /**
646
   * Load string source from db
647
   */
648
  public static function load_source($i18nstring) {
649
    // Search the database using lid if we've got it or textgroup, context otherwise
650
    $query = db_select('locales_source', 's')->fields('s');
651
    $query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
652
    $query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
653
    if (!empty($i18nstring->lid)) {
654
      $query->condition('s.lid', $i18nstring->lid);
655
    }
656
    else {
657
      $query->condition('s.textgroup', $i18nstring->textgroup);
658
      $query->condition('s.context', $i18nstring->context);
659
    }
660
    // Speed up the query, we just need one row
661
    return $query->range(0, 1)->execute()->fetchObject();
662
  }
663

    
664
  /**
665
   * Load translation from db
666
   *
667
   * @todo Optimize when we've already got the source string
668
   */
669
  public static function load_translation($i18nstring, $langcode) {
670
    // Search the database using lid if we've got it or textgroup, context otherwise
671
    if (!empty($i18nstring->lid)) {
672
      // We've already got lid, we just need translation data
673
      $query = db_select('locales_target', 't');
674
      $query->condition('t.lid', $i18nstring->lid);
675
    }
676
    else {
677
      // Still don't have lid, load string properties too
678
      $query = db_select('i18n_string', 's')->fields('s');
679
      $query->leftJoin('locales_target', 't', 's.lid = t.lid');
680
      $query->condition('s.textgroup', $i18nstring->textgroup);
681
      $query->condition('s.context', $i18nstring->context);
682
    }
683
    // Add translation fields
684
    $query->fields('t', array('translation', 'i18n_status'));
685
    $query->condition('t.language', $langcode);
686
    // Speed up the query, we just need one row
687
    $query->range(0, 1);
688
    return $query->execute()->fetchObject();
689
  }
690

    
691
  /**
692
   * Save / update string object
693
   *
694
   * There seems to be a race condition sometimes so skip errors, #277711
695
   *
696
   * @param $string
697
   *   Full string object to be saved
698
   * @param $source
699
   *   Source string object
700
   */
701
  protected function save_string($string, $update = FALSE) {
702
    if (!$string->get_source()) {
703
      // Create source string so we get an lid
704
      $this->save_source($string);
705
    }
706

    
707
    // Convert objectid to objectkey if it's numeric.
708
    if (!isset($string->objectkey)) {
709
      if (is_numeric($string->objectid)) {
710
        $string->objectkey = (int)$string->objectid;
711
      }
712
    }
713

    
714
    // Make sure objectkey is numeric.
715
    if (!is_numeric($string->objectkey)) {
716
      $string->objectkey = 0;
717
    }
718

    
719
    if (!isset($string->format)) {
720
      $string->format = '';
721
    }
722
    $status = db_merge('i18n_string')
723
      ->key(array('lid' => $string->lid))
724
      ->fields(array(
725
          'textgroup' => $string->textgroup,
726
          'context' => $string->context,
727
          'objectid' => $string->objectid,
728
          'type' => $string->type,
729
          'property' => $string->property,
730
          'objectindex' => $string->objectkey,
731
          'format' => $string->format,
732
      ))
733
      ->execute();
734
    return $status;
735
  }
736

    
737
  /**
738
   * Save translation to the db
739
   *
740
   * @param $string
741
   *   Full string object with translation data (language, translation)
742
   */
743
  protected function save_translation($string, $langcode) {
744
    db_merge('locales_target')
745
      ->key(array('lid' => $string->lid, 'language' => $langcode))
746
      ->fields(array('translation' => $string->get_translation($langcode)))
747
      ->execute();
748
  }
749

    
750
  /**
751
   * Save source string (create / update)
752
   */
753
  protected static function save_source($source) {
754
    if (isset($source->string)) {
755
      $source->source = $source->string;
756
    }
757
    if (empty($source->version)) {
758
      $source->version = 1;
759
    }
760
    return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
761
  }
762

    
763
  /**
764
   * Remove source and translations for user defined string.
765
   *
766
   * Though for most strings the 'name' or 'string id' uniquely identifies that string,
767
   * there are some exceptions (like profile categories) for which we need to use the
768
   * source string itself as a search key.
769
   *
770
   * @param $context
771
   *   Textgroup and location glued with ':'.
772
   * @param $string
773
   *   Optional source string (string in default language).
774
   */
775
  public function context_remove($context, $string = NULL, $options = array()) {
776
    $options += array('messages' => $this->debug);
777
    $i18nstring = $this->build_string($context, $string);
778
    $status = $this->string_remove($i18nstring, $options);
779

    
780
    return $this;
781
  }
782

    
783
  /**
784
   * Translate source string
785
   */
786
  public function context_translate($context, $string, $options = array()) {
787
    $i18nstring = $this->build_string($context, $string);
788
    return $this->string_translate($i18nstring, $options);
789
  }
790

    
791
  /**
792
   * Update / create translation source for user defined strings.
793
   *
794
   * @param $name
795
   *   Textgroup and location glued with ':'.
796
   * @param $string
797
   *   Source string in default language. Default language may or may not be English.
798
   * @param $options
799
   *   Array with additional options:
800
   *   - 'format', String format if the string has text format.
801
   *   - 'messages', Whether to print out status messages.
802
   *   - 'check', whether to check string format and then update/delete if not allowed.
803
   */
804
  public function context_update($context, $string, $options = array()) {
805
    $options += array('format' => FALSE, 'messages' => $this->debug, 'watchdog' => TRUE, 'check' => TRUE);
806
    $i18nstring = $this->build_string($context, $string);
807
    $i18nstring->format = $options['format'];
808
    $this->string_update($i18nstring, $options);
809
    return $this;
810
  }
811

    
812
  /**
813
   * Build combinations of an array of arrays respecting keys.
814
   *
815
   * Example:
816
   *   array(array(a,b), array(1,2)) will translate into
817
   *   array(a,1), array(a,2), array(b,1), array(b,2)
818
   */
819
  protected static function multiple_combine($properties) {
820
    $combinations = array();
821
    // Get first key, value. We need to make sure the array pointer is reset.
822
    $value = reset($properties);
823
    $key = key($properties);
824
    array_shift($properties);
825
    $values = is_array($value) ? $value : array($value);
826
    foreach ($values as $value) {
827
      if ($properties) {
828
        foreach (self::multiple_combine($properties) as $merge) {
829
          $combinations[] = array_merge(array($key => $value), $merge);
830
        }
831
      }
832
      else {
833
        $combinations[] = array($key => $value);
834
      }
835
    }
836
    return $combinations;
837
  }
838

    
839
  /**
840
   * Get multiple translations with search conditions.
841
   *
842
   * @param $translations
843
   *   Array of translation objects as loaded from the db.
844
   * @param $langcode
845
   *   Language code, array of language codes or * to search all translations.
846
   *
847
   * @return array
848
   *   Array of i18n string objects.
849
   */
850
  protected function multiple_translation_build($translations, $langcode) {
851
    $strings = array();
852
    foreach ($translations as $translation) {
853
      // The string object may be already in list
854
      if (isset($strings[$translation->context])) {
855
        $string = $strings[$translation->context];
856
      }
857
      else {
858
        $string = $this->build_string($translation->context);
859
        $string->set_properties($translation);
860
        $strings[$string->context] = $string;
861
      }
862
      // If this is a translation we set it there too
863
      if ($translation->language && $translation->translation) {
864
        $string->set_translation($translation);
865
      }
866
      elseif ($langcode) {
867
        // This may only happen when we have a source string but not translation.
868
        $string->set_translation(FALSE, $langcode);
869
      }
870
    }
871
    return $strings;
872
  }
873

    
874
  /**
875
   * Load multiple translations from db
876
   *
877
   * @todo Optimize when we've already got the source object
878
   *
879
   * @param $conditions
880
   *   Array of field values to use as query conditions.
881
   * @param $langcode
882
   *   Language code to search.
883
   * @param $index
884
   *   Field to use as index for the result.
885
   * @return array
886
   *   Array of string objects with translation set.
887
   */
888
  protected function multiple_translation_load($conditions, $langcode) {
889
    $conditions += array(
890
      'language' => $langcode,
891
      'textgroup' => $this->textgroup
892
    );
893
    // We may be querying all translations at the same time or just one language.
894
    // The language field needs some special treatment though.
895
    $query = db_select('i18n_string', 's')->fields('s');
896
    $query->leftJoin('locales_target', 't', 's.lid = t.lid');
897
    $query->fields('t', array('translation', 'language', 'i18n_status'));
898
    foreach ($conditions as $field => $value) {
899
      // Single array value, reduce array
900
      if (is_array($value) && count($value) == 1) {
901
        $value = reset($value);
902
      }
903
      if ($value === '*') {
904
        continue;
905
      }
906
      elseif ($field == 'language') {
907
        $query->condition('t.language', $value);
908
      }
909
      else {
910
        $query->condition('s.' . $field, $value);
911
      }
912
    }
913
    return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
914
  }
915

    
916
  /**
917
   * Search multiple translations with key combinations.
918
   *
919
   * Each $context field may be a single value, an array of values or '*'.
920
   * Example:
921
   * 	array('term', array(1,2), '*')
922
   * This will be mapped into the following conditions (provided language code is 'es')
923
   *  array('type' => 'term', 'objectid' => array(1,2), 'property' => '*', 'language' => 'es')
924
   * And will result in these combinations to search for
925
   *  array('type' => 'term', 'objectid' => 1, 'property' => '*', 'language' => 'es')
926
   *  array('type' => 'term', 'objectid' => 2, 'property' => '*', 'language' => 'es')
927
   *
928
   * @param $context array
929
   *   Array with String context conditions.
930
   *
931
   * @return
932
   *   Array of translation objects indexed by context.
933
   */
934
  public function multiple_translation_search($context, $langcode) {
935
    // First, build conditions and identify the variable field.
936
    $keys = array('type', 'objectid', 'property');
937
    $conditions = array_combine($keys, $context) + array('language' => $langcode);
938
    // Find existing searches in cache, compile remaining ones.
939
    $translations = $search = array();
940
    foreach ($this->multiple_combine($conditions) as $combination) {
941
      $cached = $this->multiple_cache_get($combination);
942
      if (isset($cached)) {
943
        // Cache hit. Merge and remove value from search.
944
        $translations += $cached;
945
      }
946
      else {
947
        // Not in cache, add to search conditions skipping duplicated values.
948
        // As array_merge_recursive() has some bug in PHP 5.2, http://drupal.org/node/1244598
949
        // we use our simplified version here, instead of $search = array_merge_recursive($search, $combination);
950
        foreach ($combination as $key => $value) {
951
          if (!isset($search[$key]) || !in_array($value, $search[$key], TRUE)) {
952
            $search[$key][] = $value;
953
          }
954
        }
955
      }
956
    }
957
    // If we've got any search values left, find translations.
958
    if ($search) {
959
      // Load translations for conditions and set them to the cache
960
      $loaded = $this->multiple_translation_load($search, $langcode);
961
      if ($loaded) {
962
        $translations += $loaded;
963
      }
964
      // Set cache for each of the multiple search keys.
965
      foreach ($this->multiple_combine($search) as $combination) {
966
        $list = $loaded ? $this->string_filter($loaded, $combination) : array();
967
        $this->multiple_cache_set($combination, $list);
968
      }
969
    }
970
    return $translations;
971
  }
972

    
973
  /**
974
   * Set multiple cache.
975
   *
976
   * @param $context
977
   *   String context with language property at the end.
978
   * @param $strings
979
   *   Array of strings (may be empty) to cache.
980
   */
981
  protected function multiple_cache_set($context, $strings) {
982
    $cache_key = implode(':', $context);
983
    $this->cache_multiple[$cache_key] = $strings;
984
  }
985

    
986
  /**
987
   * Get strings from multiple cache.
988
   *
989
   * @param $context array
990
   *   String context as array with language property at the end.
991
   *
992
   * @return mixed
993
   *   Array of strings (may be empty) if we've got a cache hit.
994
   *   Null otherwise.
995
   */
996
  protected function multiple_cache_get($context) {
997
    $cache_key = implode(':', $context);
998
    if (isset($this->cache_multiple[$cache_key])) {
999
      return $this->cache_multiple[$cache_key];
1000
    }
1001
    else {
1002
      // Now we try more generic keys. For instance, if we are searching 'term:1:*'
1003
      // we may try too 'term:*:*' and filter out the results.
1004
      foreach ($context as $key => $value) {
1005
        if ($value != '*') {
1006
          $try = array_merge($context, array($key => '*'));
1007
          $cache_key = implode(':', $try);
1008
          if (isset($this->cache_multiple[$cache_key])) {
1009
            // As we've found some more generic key, we need to filter using original conditions.
1010
            $strings = $this->string_filter($this->cache_multiple[$cache_key], $context);
1011
            return $strings;
1012
          }
1013
        }
1014
      }
1015
      // If we've reached here, we didn't find any cache match.
1016
      return NULL;
1017
    }
1018
  }
1019

    
1020
  /**
1021
   * Translate array of source strings
1022
   *
1023
   * @param $context
1024
   *   Context array with placeholders (*)
1025
   * @param $strings
1026
   *   Optional array of source strings indexed by the placeholder property
1027
   *
1028
   * @return array
1029
   *   Array of string objects (with translation) indexed by the placeholder field
1030
   */
1031
  public function multiple_translate($context, $strings = array(), $options = array()) {
1032
    // First, build conditions and identify the variable field
1033
    $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
1034
    $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
1035
    // If we've got keyed source strings set the array of keys on the placeholder field
1036
    // or if not, remove that condition so we search all strings with that keys.
1037
    foreach ($search as $field => $value) {
1038
      if ($value === '*') {
1039
        $property = $field;
1040
        if ($strings) {
1041
          $search[$field] = array_keys($strings);
1042
        }
1043
      }
1044
    }
1045
    // Now we'll add the language code to conditions and get the translations indexed by the property field
1046
    $result = $this->multiple_translation_search($search, $langcode);
1047
    // Remap translations using property field. If we've got strings it is important that they are in the same order.
1048
    $translations = $strings;
1049
    foreach ($result as $key => $i18nstring) {
1050
      $translations[$i18nstring->$property] = $i18nstring;
1051
    }
1052
    // Set strings as source or create
1053
    foreach ($strings as $key => $source) {
1054
      if (isset($translations[$key]) && is_object($translations[$key])) {
1055
        $translations[$key]->set_string($source);
1056
      }
1057
      else {
1058
        // Not found any string for this property, create it to map in the response
1059
        // But make sure we set this language's translation to FALSE so we don't search again
1060
        $newcontext = $context;
1061
        $newcontext[$property] = $key;
1062
        $translations[$key] = $this->build_string($newcontext)
1063
          ->set_string($source)
1064
          ->set_translation(FALSE, $langcode);
1065
      }
1066
    }
1067
    return $translations;
1068
  }
1069

    
1070
  /**
1071
   * Update string translation, only if source exists.
1072
   *
1073
   * @param $context
1074
   *   String context as array
1075
   * @param $langcode
1076
   *   Language code to create the translation for
1077
   * @param $translation
1078
   *   String translation for this language
1079
   */
1080
  function update_translation($context, $langcode, $translation) {
1081
    $i18nstring = $this->build_string($context);
1082
    if ($source = $i18nstring->get_source()) {
1083
      $source->set_translation($translation, $langcode);
1084
      $this->save_translation($source, $langcode);
1085
      return $source;
1086
    }
1087
  }
1088

    
1089
  /**
1090
   * Recheck strings after update
1091
   */
1092
  public function update_check() {
1093
    // Find strings in locales_source that have no data in i18n_string
1094
    $query = db_select('locales_source', 'l')
1095
    ->fields('l')
1096
    ->condition('l.textgroup', $this->textgroup);
1097
    $alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
1098
    $query->isNull('s.lid');
1099
    foreach ($query->execute()->fetchAll() as $string) {
1100
      $i18nstring = $this->build_string($string->context, $string->source);
1101
      $this->save_string($i18nstring);
1102
    }
1103
  }
1104

    
1105
}
1106

    
1107
/**
1108
 * String object wrapper
1109
 */
1110
class i18n_string_object_wrapper extends i18n_object_wrapper {
1111
  // Text group object
1112
  protected $textgroup;
1113
  // Properties for translation
1114
  protected $properties;
1115

    
1116
  /**
1117
   * Get object strings for translation
1118
   *
1119
    * This will return a simple array of string objects, indexed by full string name.
1120
    *
1121
    * @param $options
1122
    *   Array with processing options.
1123
    *   - 'empty', whether to return empty strings, defaults to FALSE.
1124
   */
1125
  public function get_strings($options = array()) {
1126
    $options += array('empty' => FALSE);
1127
    $strings = array();
1128
    foreach ($this->get_properties() as $textgroup => $textgroup_list) {
1129
      foreach ($textgroup_list as $type => $type_list) {
1130
        foreach ($type_list as $object_id => $object_list) {
1131
          foreach ($object_list as $key => $string) {
1132
            if ($options['empty'] || !empty($string['string'])) {
1133
              // Build string object, that will trigger static caches everywhere.
1134
              $i18nstring = i18n_string_textgroup($textgroup)
1135
                ->build_string(array($type, $object_id, $key))
1136
                ->set_string($string);
1137
              $strings[$i18nstring->get_name()] = $i18nstring;
1138
            }
1139
          }
1140
        }
1141
      }
1142
    }
1143
    return $strings;
1144
  }
1145
  /**
1146
   * Get object translatable properties
1147
   *
1148
   * This will return a big array indexed by textgroup, object type, object id and string key.
1149
   * Each element is an array with string information, and may have these properties:
1150
   * - 'string', the string itself, will be NULL if the object doesn't have that string
1151
   * - 'format', string format when needed
1152
   * - 'title', string readable name
1153
   */
1154
  public function get_properties() {
1155
    if (!isset($this->properties)) {
1156
      $this->properties = $this->build_properties();
1157
      // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
1158
      drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object);
1159

    
1160
    }
1161
    return $this->properties;
1162
  }
1163

    
1164
  /**
1165
   * Build properties from object.
1166
   */
1167
  protected function build_properties() {
1168
    list($string_type, $object_id) = $this->get_string_context();
1169
    $object_keys = array(
1170
      $this->get_textgroup(),
1171
      $string_type,
1172
      $object_id,
1173
    );
1174
    $strings = array();
1175
    foreach ($this->get_string_info('properties', array()) as $field => $info) {
1176
      $info = is_array($info) ? $info : array('title' => $info);
1177
      $field_name = isset($info['field']) ? $info['field'] : $field;
1178
      $value = $this->get_field($field_name);
1179
      if (is_array($value) && isset($value['value'])) {
1180
        $format = isset($value['format']) ? $value['format'] : NULL;
1181
        $value = $value['value'];
1182
      }
1183
      else {
1184
        $format = isset($info['format']) ? $this->get_field($info['format']) : NULL;
1185
      }
1186
      $strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
1187
        'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
1188
        'title' => $info['title'],
1189
        'format' => $format,
1190
        'name' => array_merge($object_keys, array($field)),
1191
      );
1192
    }
1193
    return $strings;
1194
  }
1195

    
1196
  /**
1197
   * Get string context
1198
   */
1199
  public function get_string_context() {
1200
    return array($this->get_string_info('type'), $this->get_key());
1201
  }
1202

    
1203
  /**
1204
   * Get translate path for object
1205
   *
1206
   * @param $langcode
1207
   * 	 Language code if we want ti for a specific language
1208
   */
1209
  public function get_translate_path($langcode = NULL) {
1210
    $replacements = array('%i18n_language' => $langcode ? $langcode : '');
1211
    if ($path = $this->get_string_info('translate path')) {
1212
      return $this->path_replace($path, $replacements);
1213
    }
1214
    elseif ($path = $this->get_info('translate tab')) {
1215
      // If we've got a translate tab path, we just add language to it
1216
      return $this->path_replace($path . '/%i18n_language', $replacements);
1217
    }
1218
  }
1219

    
1220
  /**
1221
   * Translation mode for object
1222
   */
1223
  public function get_translate_mode() {
1224
    return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
1225
  }
1226

    
1227
  /**
1228
   * Get textgroup name
1229
   */
1230
  public function get_textgroup() {
1231
    return $this->get_string_info('textgroup');
1232
  }
1233

    
1234
  /**
1235
   * Get textgroup object
1236
   */
1237
  protected function textgroup() {
1238
    if (!isset($this->textgroup)) {
1239
      $this->textgroup = i18n_string_textgroup($this->get_textgroup());
1240
    }
1241
    return $this->textgroup;
1242
  }
1243

    
1244
  /**
1245
   * Translate object.
1246
   *
1247
   * Translations are cached so it runs only once per language.
1248
   *
1249
   * @return object/array
1250
   *   A clone of the object with its properties translated.
1251
   */
1252
  public function translate($langcode, $options = array()) {
1253
    // We may have it already translated. As objects are statically cached, translations are too.
1254
    if (!isset($this->translations[$langcode])) {
1255
      $this->translations[$langcode] = $this->translate_object($langcode, $options);
1256
    }
1257
    return $this->translations[$langcode];
1258
  }
1259

    
1260
  /**
1261
   * Translate access (localize strings)
1262
   */
1263
  protected function localize_access() {
1264
    // We could check also whether the object has strings to translate:
1265
    //   && $this->get_strings(array('empty' => TRUE))
1266
    // However it may be better to display the 'No available strings' message
1267
    // for the user to have a clue of what's going on. See i18n_string_translate_page_object()
1268
    return user_access('translate interface') && user_access('translate user-defined strings');
1269
  }
1270

    
1271
  /**
1272
   * Translate all properties for object.
1273
   *
1274
   * On top of object strings we search for all textgroup:type:objectid:* properties
1275
   *
1276
   * @param $langcode
1277
   *   A clone of the object or array
1278
   */
1279
  protected function translate_object($langcode, $options) {
1280
    // Clone object or array so we don't affect the original one.
1281
    $object = is_object($this->object) ? clone $this->object : $this->object;
1282
    // Get object strings for translatable properties.
1283
    if ($strings = $this->get_strings()) {
1284
      // We preload some of the property translations with a single query.
1285
      if ($context = $this->get_translate_context($langcode, $options)) {
1286
        $found = $this->textgroup()->multiple_translation_search($context, $langcode);
1287
      }
1288
      // Replace all strings in object.
1289
      foreach ($strings as $i18nstring) {
1290
        $this->translate_field($object, $i18nstring, $langcode, $options);
1291
      }
1292
    }
1293
    return $object;
1294
  }
1295

    
1296
  /**
1297
   * Context to be pre-loaded before translation.
1298
   */
1299
  protected function get_translate_context($langcode, $options) {
1300
    // One-query translation of all textgroup:type:objectid:* properties
1301
    $context = $this->get_string_context();
1302
    $context[] = '*';
1303
    return $context;
1304
  }
1305

    
1306
  /**
1307
   * Translate object property.
1308
   *
1309
   * Mot often, this is a direct field set, but sometimes fields may have different formats.
1310
   */
1311
  protected function translate_field(&$object, $i18nstring, $langcode, $options) {
1312
    $field_name = $i18nstring->property;
1313
    $translation = $i18nstring->format_translation($langcode, $options);
1314
    if (is_object($object)) {
1315
      $object->$field_name = $translation;
1316
    }
1317
    elseif (is_array($object)) {
1318
      $object[$field_name] = $translation;
1319
    }
1320
  }
1321

    
1322
  /**
1323
   * Remove all strings for this object.
1324
   */
1325
  public function strings_remove($options = array()) {
1326
    $result = array();
1327
    foreach ($this->load_strings() as $key => $string) {
1328
      $result[$key] = $string->remove($options);
1329
    }
1330
    return _i18n_string_result_count($result);
1331
  }
1332

    
1333
  /**
1334
   * Update all strings for this object.
1335
   */
1336
  public function strings_update($options = array()) {
1337
    $options += array('empty' => TRUE, 'update' => TRUE);
1338
    $result = array();
1339
    $existing = $this->load_strings();
1340
    // Update object strings
1341
    foreach ($this->get_strings($options) as $key => $string) {
1342
      $result[$key] = $string->update($options);
1343
      unset($existing[$key]);
1344
    }
1345
    // Delete old existing strings.
1346
    foreach ($existing as $key => $string) {
1347
      $result[$key] = $string->remove($options);
1348
    }
1349
    return _i18n_string_result_count($result);
1350
  }
1351

    
1352
  /**
1353
   * Load all existing strings for this object.
1354
   */
1355
  public function load_strings() {
1356
    list($type, $id) = $this->get_string_context();
1357
    return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
1358
  }
1359
}
1360

    
1361

    
1362
/**
1363
 * Textgroup handler for i18n_string API which integrated persistent caching.
1364
 */
1365
class i18n_string_textgroup_cached extends i18n_string_textgroup_default {
1366

    
1367
  /**
1368
   * Defines the timeout for the persistent caching.
1369
   * @var int
1370
   */
1371
  public $caching_time = CACHE_TEMPORARY;
1372

    
1373
  /**
1374
   * Extends the existing constructor with a cache handling.
1375
   *
1376
   * @param string $textgroup
1377
   *   The name of this textgroup.
1378
   */
1379
  public function __construct($textgroup) {
1380
    parent::__construct($textgroup);
1381
    // Fetch persistent caches, the persistent caches contain only metadata.
1382
    // Those metadata are processed by the related cache_get() methods.
1383
    foreach (array('cache_multiple', 'strings') as $caches_type) {
1384
      if (($cache = cache_get('i18n:string:tgroup:' . $this->textgroup . ':' . $caches_type)) && !empty($cache->data)) {
1385
        $this->{$caches_type} = $cache->data;
1386
      }
1387
    }
1388
  }
1389

    
1390
  /**
1391
   * Class destructor.
1392
   *
1393
   * Updates the persistent caches for the next usage.
1394
   * This function not only stores the data of the textgroup objects but also
1395
   * of the string objects. That way we ensure that only cacheable string object
1396
   * go into the persistent cache.
1397
   */
1398
  public function __destruct() {
1399
    // Reduce size to cache by removing NULL values.
1400
    $this->strings = array_filter($this->strings);
1401

    
1402

    
1403
    $strings_to_cache = array();
1404
    // Store the persistent caches. We just store the metadata the translations
1405
    // are stored by the string object itself. However storing the metadata
1406
    // reduces the number of DB queries executed during runtime.
1407
    $cache_data = array();
1408
    foreach ($this->strings as $context => $i18n_string_object) {
1409
      $cache_data[$context] = $context;
1410
      $strings_to_cache[$context] = $i18n_string_object;
1411
    }
1412
    cache_set('i18n:string:tgroup:' . $this->textgroup . ':strings', $cache_data, 'cache', $this->caching_time);
1413

    
1414
    $cache_data = array();
1415
    foreach ($this->cache_multiple as $pattern => $strings) {
1416
      foreach ($strings as $context => $i18n_string_object) {
1417
        $cache_data[$pattern][$context] = $context;
1418
        $strings_to_cache[$context] = $i18n_string_object;
1419
      }
1420
    }
1421
    cache_set('i18n:string:tgroup:' . $this->textgroup . ':cache_multiple', $cache_data, 'cache', $this->caching_time);
1422

    
1423
    // Cache the string objects related to this textgroup.
1424
    // Store only the public visible data into the persistent cache.
1425
    foreach ($strings_to_cache as $i18n_string_object) {
1426
      // If this isn't an object it's an unprocessed cache item and doesn't need
1427
      // to be stored again.
1428
      if (is_object($i18n_string_object)) {
1429
        cache_set($i18n_string_object->get_cid(), get_object_vars($i18n_string_object), 'cache', $this->caching_time);
1430
      }
1431
    }
1432
  }
1433

    
1434
  /**
1435
   * Reset cache, needed for tests.
1436
   *
1437
   * Takes care of the persistent caches.
1438
   */
1439
  public function cache_reset() {
1440
    // Reset the persistent caches.
1441
    cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
1442
    // Reset the complete string object cache too. This will affect string
1443
    // objects of other textgroups as well.
1444
    cache_clear_all('i18n:string:obj:', 'cache', TRUE);
1445

    
1446
    return parent::cache_reset();
1447
  }
1448

    
1449
  /**
1450
   * Get translation from cache.
1451
   *
1452
   * Extends the original handler with persistent caching.
1453
   *
1454
   * @param string $context
1455
   *   The context to look out for.
1456
   * @return i18n_string_object|NULL
1457
   *   The string object if available or NULL otherwise.
1458
   */
1459
  protected function cache_get($context) {
1460
    if (isset($this->strings[$context])) {
1461
      // If the cache contains a string re-build i18n_string_object.
1462
      if (is_string($this->strings[$context])) {
1463
        $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
1464
        $i8n_string_object->set_context($context);
1465
        $this->strings[$context] = $i8n_string_object;
1466
      }
1467
      // Now run the original handling.
1468
      return parent::cache_get($context);
1469
    }
1470
    return NULL;
1471
  }
1472

    
1473
  /**
1474
   * Get strings from multiple cache.
1475
   *
1476
   * @param $context array
1477
   *   String context as array with language property at the end.
1478
   *
1479
   * @return mixed
1480
   *   Array of strings (may be empty) if we've got a cache hit.
1481
   *   Null otherwise.
1482
   */
1483
  protected function multiple_cache_get($context) {
1484
    // Ensure the values from the persistent cache are properly re-build.
1485
    $cache_key = implode(':', $context);
1486
    if (isset($this->cache_multiple[$cache_key])) {
1487
      foreach ($this->cache_multiple[$cache_key] as $cached_context) {
1488
        if (is_string($cached_context)) {
1489
          $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
1490
          $i8n_string_object->set_context($cached_context);
1491
          $this->cache_multiple[$cache_key][$cached_context] = $i8n_string_object;
1492
        }
1493
      }
1494
    }
1495
    else {
1496
      // Now we try more generic keys. For instance, if we are searching 'term:1:*'
1497
      // we may try too 'term:*:*' and filter out the results.
1498
      foreach ($context as $key => $value) {
1499
        if ($value != '*') {
1500
          $try = array_merge($context, array($key => '*'));
1501
          $cached_results = $this->multiple_cache_get($try);
1502
          // Now filter the ones that actually match.
1503
          if (!empty($cached_results)) {
1504
            $cached_results = $this->string_filter($cached_results, $context);
1505
          }
1506
          return $cached_results;
1507
        }
1508
      }
1509
    }
1510
    return parent::multiple_cache_get($context);
1511
  }
1512

    
1513
  public function string_update($i18nstring, $options = array()) {
1514
    // Flush persistent cache.
1515
    cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
1516
    return parent::string_update($i18nstring, $options);
1517
  }
1518

    
1519
  public function string_remove($i18nstring, $options = array()) {
1520
    // Flush persistent cache.
1521
    cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
1522
    return parent::string_remove($i18nstring, $options);
1523
  }
1524
}