Projet

Général

Profil

Paste
Télécharger (42 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / i18n / i18n_string / i18n_string.inc @ 74f6bef0

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;
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
  }
43
  /**
44
   * Get message parameters from context and string.
45
   */
46
  public function get_args() {
47
    return array(
48
      '%location' => $this->location,
49
      '%textgroup' => $this->textgroup,
50
      '%string' => ($string = $this->get_string()) ? $string : t('[empty string]'),
51
    );
52
  }
53
  /**
54
   * Set context properties
55
   */
56
  public function set_context($context) {
57
    $parts = is_array($context) ? $context : explode(':', $context);
58
    $this->context = is_array($context) ? implode(':', $context) : $context;
59
    // Location will be the full string name
60
    $this->location = $this->textgroup . ':' . $this->context;
61
    $this->type = array_shift($parts);
62
    $this->objectid = $parts ? array_shift($parts) : '';
63
    $this->objectkey = (int)$this->objectid;
64
    // Remaining elements glued again with ':'
65
    $this->property = $parts ? implode(':', $parts) : '';
66
    return $this;
67
  }
68
  /**
69
   * Get string name including textgroup and context
70
   */
71
  public function get_name() {
72
    return $this->textgroup . ':' . $this->type . ':' . $this->objectid . ':' . $this->property;
73
  }
74
  /**
75
   * Get source string
76
   */
77
  public function get_string() {
78
    if (isset($this->string)) {
79
      return $this->string;
80
    }
81
    elseif (isset($this->source)) {
82
      return $this->source;
83
    }
84
    elseif ($this->textgroup()->debug) {
85
      return empty($this->lid) ? t('[Source not found]') : t('[String not found]');
86
    }
87
    else {
88
      return '';
89
    }
90
  }
91
  /**
92
   * Set source string
93
   *
94
   * @param $string
95
   *   Plain string or array with 'string', 'format', etc...
96
   */
97
  public function set_string($string) {
98
    if (is_array($string)) {
99
      $this->string = isset($string['string']) ? $string['string'] : NULL;
100
      if (isset($string['format'])) {
101
        $this->format = $string['format'];
102
      }
103
    }
104
    else {
105
      $this->string = $string;
106
    }
107
    if (isset($string['title'])) {
108
      $this->title = $string['title'];
109
    }
110
    return $this;
111
  }
112
  /**
113
   * Get string title.
114
   */
115
  public function get_title() {
116
    return isset($this->title) ? $this->title : t('String');
117
  }
118
  /**
119
   * Get translation to language from string object
120
   */
121
  public function get_translation($langcode) {
122
    if (!isset($this->translations[$langcode])) {
123
      $translation = $this->textgroup()->load_translation($this, $langcode);
124
      if ($translation && isset($translation->translation)) {
125
        $this->set_translation($translation, $langcode);
126
      }
127
      else {
128
        // No source, no translation
129
        $this->translations[$langcode] = FALSE;
130
      }
131
    }
132
    // Which doesn't mean we've got a translation, only that we've got the result cached
133
    return $this->translations[$langcode];
134
  }
135
  /**
136
   * Set translation for language
137
   *
138
   * @param $translation
139
   *   Translation object (from database) or string
140
   */
141
  public function set_translation($translation, $langcode = NULL) {
142
    if (is_object($translation)) {
143
      $langcode = $langcode ? $langcode : $translation->language;
144
      $string = isset($translation->translation) ? $translation->translation : FALSE;
145
      $this->set_properties($translation);
146
    }
147
    else {
148
      $string = $translation;
149
    }
150
    $this->translations[$langcode] = $string;
151
    return $this;
152
  }
153

    
154
  /**
155
   * Format the resulting translation or the default string applying callbacks
156
   *
157
   * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
158
   */
159
  public function format_translation($langcode, $options = array()) {
160
    $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug);
161
    if ($translation = $this->get_translation($langcode)) {
162
      $string = $translation;
163
      if (isset($options['filter'])) {
164
        $string = call_user_func($options['filter'], $string);
165
      }
166
    }
167
    else {
168
      // Get default source string if no translation.
169
      $string = $this->get_string();
170
      $options['sanitize'] = !empty($options['sanitize default']);
171
    }
172
    if (!empty($this->format)) {
173
      $options += array('format' => $this->format);
174
    }
175
    // Add debug information if enabled
176
    if ($options['debug']) {
177
      $info = array($langcode, $this->textgroup, $this->context);
178
      if (!empty($this->format)) {
179
        $info[] = $this->format;
180
      }
181
      $options += array('suffix' => '');
182
      $options['suffix'] .= ' [' . implode(':', $info) . ']';
183
    }
184
    // Finally, apply options, filters, callback, etc...
185
    return i18n_string_format($string, $options);
186
  }
187

    
188
  /**
189
   * Get source string provided a string object.
190
   *
191
   * @return
192
   *   String object if source exists.
193
   */
194
  public function get_source() {
195
    // If already searched and not found we don't have a source,
196
    if (isset($this->lid) && !$this->lid) {
197
      return NULL;
198
    }
199
    elseif (!isset($this->lid) || !isset($this->source)) {
200
      // We may have lid from loading a translation but not loaded the source yet.
201
      if ($source = $this->textgroup()->load_source($this)) {
202
        // Set properties but don't override existing ones
203
        $this->set_properties($source, FALSE, FALSE);
204
        if (!isset($this->string)) {
205
          $this->string = $source->source;
206
        }
207
        return $this;
208
      }
209
      else {
210
        $this->lid = FALSE;
211
        return NULL;
212
      }
213
    }
214
    else {
215
      return $this;
216
    }
217
  }
218

    
219
  /**
220
   * Set properties from object or array
221
   *
222
   * @param $properties
223
   *   Obejct or array of properties
224
   * @param $set_null
225
   *   Whether to set null properties too
226
   * @param $override
227
   *   Whether to set properties that are already set in this object
228
   */
229
  public function set_properties($properties, $set_null = TRUE, $override = TRUE) {
230
    foreach ((array)$properties as $field => $value) {
231
      if (property_exists($this, $field) && ($set_null || isset($value)) && ($override || !isset($this->$field))) {
232
        $this->$field = $value;
233
      }
234
    }
235
    return $this;
236
  }
237
  /**
238
   * Access textgroup object
239
   */
240
  protected function textgroup() {
241
    if (!isset($this->_textgroup)) {
242
      $this->_textgroup = i18n_string_textgroup($this->textgroup);
243
    }
244
    return $this->_textgroup;
245
  }
246
  /**
247
   * Update this string.
248
   */
249
  public function update($options = array()) {
250
    return $this->textgroup()->string_update($this, $options);
251
  }
252
  /**
253
   * Delete this string.
254
   */
255
  public function remove($options = array()) {
256
    return $this->textgroup()->string_remove($this, $options);
257
  }
258
  /**
259
   * Check whether there is any problem for the  user to translate a this string.
260
   *
261
   * @param $account
262
   *   Optional user account, defaults to current user.
263
   *
264
   * @return
265
   *   None if the user has access to translate the string.
266
   *   Error message if the user cannot translate that string.
267
   */
268
  public function check_translate_access($account = NULL) {
269
    return i18n_string_translate_check_string($this, $account);
270
  }
271
}
272

    
273
/**
274
 * Textgroup handler for i18n_string API
275
 */
276
class i18n_string_textgroup_default {
277
  // Text group name
278
  public $textgroup;
279
  // Debug flag, set to true to print out more information.
280
  public $debug;
281
  // Cached or preloaded string objects
282
  public $strings;
283
  // Multiple translations search map
284
  protected $cache_multiple;
285

    
286
  /**
287
   * Class constructor.
288
   *
289
   * There are to hidden variables to produce debugging information:
290
   * - 'i18n_string_debug', generic for all text groups.
291
   * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
292
   */
293
  public function __construct($textgroup) {
294
    $this->textgroup = $textgroup;
295
    $this->debug = variable_get('i18n_string_debug', FALSE) || variable_get('i18n_string_debug_' . $textgroup, FALSE);
296
  }
297
  /**
298
   * Build string object
299
   *
300
   * @param $context
301
   *   Context array or string
302
   * @param $string string
303
   *   Current value for string source
304
   */
305
  public function build_string($context, $string = NULL) {
306
    // First try to locate string on cache
307
    $context = is_array($context) ? implode(':', $context) : $context;
308
    if ($cached = $this->cache_get($context)) {
309
      $i18nstring = $cached;
310
    }
311
    else {
312
      $i18nstring = new i18n_string_object();
313
      $i18nstring->textgroup = $this->textgroup;
314
      $i18nstring->set_context($context);
315
      $this->cache_set($context, $i18nstring);
316
    }
317
    if (isset($string)) {
318
      $i18nstring->set_string($string);
319
    }
320
    return $i18nstring;
321
  }
322
  /**
323
   * Add source string to the locale tables for translation.
324
   *
325
   * It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
326
   * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
327
   *
328
   * This function checks for already existing string without context for this textgroup and updates it accordingly.
329
   * It is intended for backwards compatibility, using already created strings.
330
   *
331
   * @param $i18nstring
332
   *   String object
333
   * @param $format
334
   *   Text format, for strings that will go through some filter
335
   * @return
336
   *   Update status.
337
   */
338
  protected function string_add($i18nstring, $options = array()) {
339
    $options += array('watchdog' => TRUE);
340
    // Default return status if nothing happens
341
    $status = -1;
342
    $source = NULL;
343
    $location = $i18nstring->location;
344
    // The string may not be allowed for translation depending on its format.
345
    if (!$this->string_check($i18nstring, $options)) {
346
      // The format may have changed and it's not allowed now, delete the source string
347
      return $this->string_remove($i18nstring, $options);
348
    }
349
    elseif ($source = $i18nstring->get_source()) {
350
      if ($source->source != $i18nstring->string || $source->location != $location) {
351
        $i18nstring->location = $location;
352
        // String has changed, mark translations for update
353
        $status = $this->save_source($i18nstring);
354
        db_update('locales_target')
355
          ->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))
356
          ->condition('lid', $source->lid)
357
          ->execute();
358
      }
359
      elseif (empty($source->version)) {
360
        // When refreshing strings, we've done version = 0, update it
361
        $this->save_source($i18nstring);
362
      }
363
    }
364
    else {
365
      // We don't have the source object, create it
366
      $status = $this->save_source($i18nstring);
367
    }
368
    // Make sure we have i18n_string part, create or update
369
    // This will also create the source object if doesn't exist
370
    $this->save_string($i18nstring);
371

    
372
    if ($options['watchdog']) {
373
      switch ($status) {
374
        case SAVED_UPDATED:
375
          watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
376
          break;
377
        case SAVED_NEW:
378
          watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
379
          break;
380
      }
381
    }
382
    return $status;
383
  }
384

    
385
  /**
386
   * Check if string is ok for translation
387
   */
388
  protected static function string_check($i18nstring, $options = array()) {
389
    $options += array('messages' => FALSE, 'watchdog' => TRUE);
390
    if (!empty($i18nstring->format) && !i18n_string_allowed_format($i18nstring->format)) {
391
      // This format is not allowed, so we remove the string, in this case we produce a warning
392
      drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
393
      return FALSE;
394
    }
395
    else {
396
      return TRUE;
397
    }
398
  }
399

    
400
  /**
401
   * Filter array of strings
402
   *
403
   * @param $filter
404
   *   Array of name value conditions.
405
   */
406
  protected static function string_filter($string_list, $filter) {
407
    // Remove 'language' and '*' conditions.
408
    if (isset($filter['language'])) {
409
      unset($filter['language']);
410
    }
411
    while ($field = array_search('*', $filter)) {
412
      unset($filter[$field]);
413
    }
414
    foreach ($string_list as $key => $string) {
415
      foreach ($filter as $field => $value) {
416
        if ($string->$field != $value) {
417
          unset($string_list[$key]);
418
          break;
419
        }
420
      }
421
    }
422
    return $string_list;
423
  }
424

    
425
  /**
426
   * Build query for i18n_string table
427
   */
428
  protected static function string_query($context, $multiple = FALSE) {
429
    // Search the database using lid if we've got it or textgroup, context otherwise
430
    $query = db_select('i18n_string', 's')->fields('s');
431
    if (!empty($context->lid)) {
432
      $query->condition('s.lid', $context->lid);
433
    }
434
    else {
435
      $query->condition('s.textgroup', $context->textgroup);
436
      if (!$multiple) {
437
        $query->condition('s.context', $context->context);
438
      }
439
      else {
440
        // Query multiple strings
441
        foreach (array('type', 'objectid', 'property') as $field) {
442
          if (!empty($context->$field)) {
443
            $query->condition('s.' . $field, $context->$field);
444
          }
445
        }
446
      }
447
    }
448
    return $query;
449
  }
450

    
451
  /**
452
   * Remove string object.
453
   *
454
   * @return
455
   *   SAVED_DELETED | FALSE (If the operation failed because no source)
456
   */
457
  public function string_remove($i18nstring, $options = array()) {
458
    $options += array('watchdog' => TRUE, 'messages' => $this->debug);
459
    if ($source = $i18nstring->get_source()) {
460
      db_delete('locales_target')->condition('lid', $source->lid)->execute();
461
      db_delete('i18n_string')->condition('lid', $source->lid)->execute();
462
      db_delete('locales_source')->condition('lid', $source->lid)->execute();
463
      $this->cache_set($source->context, NULL);
464
      if ($options['watchdog']) {
465
        watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
466
      }
467
      if ($options['messages']) {
468
        drupal_set_message(t('Deleted string %location for text group %textgroup: %string', $i18nstring->get_args()));
469
      }
470
      return SAVED_DELETED;
471
    }
472
    else {
473
      if ($options['messages']) {
474
        drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
475
      }
476
      return FALSE;
477
    }
478
  }
479

    
480
  /**
481
   * Translate string object
482
   *
483
   * @param $i18nstring
484
   *   String object
485
   * @param $options
486
   *   Array with aditional options
487
   */
488
  protected function string_translate($i18nstring, $options = array()) {
489
    $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
490
    // Search for existing translation (result will be cached in this function call)
491
    $i18nstring->get_translation($langcode);
492
    return $i18nstring;
493
  }
494

    
495
  /**
496
   * Update / create / remove string.
497
   *
498
   * @param $name
499
   *   String context.
500
   * @pram $string
501
   *   New value of string for update/create. May be empty for removing.
502
   * @param $format
503
   *   Text format, that must have been checked against allowed formats for translation
504
   * @param $options
505
   *   Processing options, the ones used here are:
506
   *   - 'watchdog', whether to produce watchdog messages.
507
   *   - 'messages', whether to produce user messages.
508
   *   - 'check', whether to check string format and then update/delete if not allowed.
509
   * @return status
510
   *   SAVED_UPDATED | SAVED_NEW | SAVED_DELETED | FALSE (If the string is to be removed but has no source)
511
   */
512
  public function string_update($i18nstring, $options = array()) {
513
    $options += array('watchdog' => TRUE, 'messages' => $this->debug, 'check' => TRUE);
514
    if ((!$options['check'] || $this->string_check($i18nstring, $options)) && $i18nstring->get_string()) {
515
      // String is ok, has a value so we store it into the database.
516
      $status = $this->string_add($i18nstring, $options);
517
    }
518
    elseif ($i18nstring->get_source()) {
519
      // Just remove it if we already had a source created before.
520
      $status = $this->string_remove($i18nstring, $options);
521
    }
522
    else {
523
      // String didn't pass validation or we have an empty string but was not stored anyway.
524
      $status = FALSE;
525
    }
526
    if ($options['messages']) {
527
      switch ($status) {
528
        case SAVED_UPDATED:
529
          drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
530
          break;
531
        case SAVED_NEW:
532
          drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
533
          break;
534
      }
535
    }
536
    if ($options['watchdog']) {
537
      switch ($status) {
538
        case SAVED_UPDATED:
539
          watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
540
          break;
541
        case SAVED_NEW:
542
          watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
543
          break;
544
      }
545
    }
546
    return $status;
547
  }
548

    
549
  /**
550
   * Set string object into cache
551
   */
552
  protected function cache_set($context, $string) {
553
    $this->strings[$context] = $string;
554
  }
555

    
556
  /**
557
   * Get translation from cache
558
   */
559
  protected function cache_get($context) {
560
    return isset($this->strings[$context]) ? $this->strings[$context] : NULL;
561
  }
562

    
563
  /**
564
   * Reset cache, needed for tests
565
   */
566
  public function cache_reset() {
567
    $this->strings = array();
568
    $this->string_format = array();
569
    $this->translations = array();
570
  }
571

    
572
  /**
573
   * Load multiple strings.
574
   *
575
   * @return array
576
   *   List of strings indexed by full string name.
577
   */
578
  public function load_strings($conditions = array()) {
579
    // Add textgroup condition and load all
580
    $conditions['textgroup'] = $this->textgroup;
581
    $list = array();
582
    foreach (i18n_string_load_multiple($conditions) as $string) {
583
      $list[$string->get_name()] = $string;
584
      $this->cache_set($string->context, $string);
585
    }
586
    return $list;
587
  }
588

    
589
  /**
590
   * Load string source from db
591
   */
592
  public static function load_source($i18nstring) {
593
    // Search the database using lid if we've got it or textgroup, context otherwise
594
    $query = db_select('locales_source', 's')->fields('s');
595
    $query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
596
    $query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
597
    if (!empty($i18nstring->lid)) {
598
      $query->condition('s.lid', $i18nstring->lid);
599
    }
600
    else {
601
      $query->condition('s.textgroup', $i18nstring->textgroup);
602
      $query->condition('s.context', $i18nstring->context);
603
    }
604
    // Speed up the query, we just need one row
605
    return $query->range(0, 1)->execute()->fetchObject();
606
  }
607

    
608
  /**
609
   * Load translation from db
610
   *
611
   * @todo Optimize when we've already got the source string
612
   */
613
  public static function load_translation($i18nstring, $langcode) {
614
    // Search the database using lid if we've got it or textgroup, context otherwise
615
    if (!empty($i18nstring->lid)) {
616
      // We've alreay got lid, we just need translation data
617
      $query = db_select('locales_target', 't');
618
      $query->condition('t.lid', $i18nstring->lid);
619
    }
620
    else {
621
      // Still don't have lid, load string properties too
622
      $query = db_select('i18n_string', 's')->fields('s');
623
      $query->leftJoin('locales_target', 't', 's.lid = t.lid');
624
      $query->condition('s.textgroup', $i18nstring->textgroup);
625
      $query->condition('s.context', $i18nstring->context);
626
    }
627
    // Add translation fields
628
    $query->fields('t', array('translation', 'i18n_status'));
629
    $query->condition('t.language', $langcode);
630
    // Speed up the query, we just need one row
631
    $query->range(0, 1);
632
    return $query->execute()->fetchObject();
633
  }
634

    
635
  /**
636
   * Save / update string object
637
   *
638
   * There seems to be a race condition sometimes so skip errors, #277711
639
   *
640
   * @param $string
641
   *   Full string object to be saved
642
   * @param $source
643
   *   Source string object
644
   */
645
  protected function save_string($string, $update = FALSE) {
646
    if (!$string->get_source()) {
647
      // Create source string so we get an lid
648
      $this->save_source($string);
649
    }
650
    if (!isset($string->objectkey)) {
651
      $string->objectkey = (int)$string->objectid;
652
    }
653
    if (!isset($string->format)) {
654
      $string->format = '';
655
    }
656
    $status = db_merge('i18n_string')
657
      ->key(array('lid' => $string->lid))
658
      ->fields(array(
659
          'textgroup' => $string->textgroup,
660
          'context' => $string->context,
661
          'objectid' => $string->objectid,
662
          'type' => $string->type,
663
          'property' => $string->property,
664
          'objectindex' => $string->objectkey,
665
          'format' => $string->format,
666
      ))
667
      ->execute();
668
    return $status;
669
  }
670

    
671
  /**
672
   * Save translation to the db
673
   *
674
   * @param $string
675
   *   Full string object with translation data (language, translation)
676
   */
677
  protected function save_translation($string, $langcode) {
678
    db_merge('locales_target')
679
      ->key(array('lid' => $string->lid, 'language' => $langcode))
680
      ->fields(array('translation' => $string->get_translation($langcode)))
681
      ->execute();
682
  }
683

    
684
  /**
685
   * Save source string (create / update)
686
   */
687
  protected static function save_source($source) {
688
    if (isset($source->string)) {
689
      $source->source = $source->string;
690
    }
691
    if (empty($source->version)) {
692
      $source->version = 1;
693
    }
694
    return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
695
  }
696

    
697
  /**
698
   * Remove source and translations for user defined string.
699
   *
700
   * Though for most strings the 'name' or 'string id' uniquely identifies that string,
701
   * there are some exceptions (like profile categories) for which we need to use the
702
   * source string itself as a search key.
703
   *
704
   * @param $context
705
   *   Textgroup and location glued with ':'.
706
   * @param $string
707
   *   Optional source string (string in default language).
708
   */
709
  public function context_remove($context, $string = NULL, $options = array()) {
710
    $options += array('messages' => $this->debug);
711
    $i18nstring = $this->build_string($context, $string);
712
    $status = $this->string_remove($i18nstring, $options);
713

    
714
    return $this;
715
  }
716

    
717
  /**
718
   * Translate source string
719
   */
720
  public function context_translate($context, $string, $options = array()) {
721
    $i18nstring = $this->build_string($context, $string);
722
    return $this->string_translate($i18nstring, $options);
723
  }
724

    
725
  /**
726
   * Update / create translation source for user defined strings.
727
   *
728
   * @param $name
729
   *   Textgroup and location glued with ':'.
730
   * @param $string
731
   *   Source string in default language. Default language may or may not be English.
732
   * @param $options
733
   *   Array with additional options:
734
   *   - 'format', String format if the string has text format.
735
   *   - 'messages', Whether to print out status messages.
736
   *   - 'check', whether to check string format and then update/delete if not allowed.
737
   */
738
  public function context_update($context, $string, $options = array()) {
739
    $options += array('format' => FALSE, 'messages' => $this->debug, 'watchdog' => TRUE, 'check' => TRUE);
740
    $i18nstring = $this->build_string($context, $string);
741
    $i18nstring->format = $options['format'];
742
    $this->string_update($i18nstring, $options);
743
    return $this;
744
  }
745

    
746
  /**
747
   * Build combinations of an array of arrays respecting keys.
748
   *
749
   * Example:
750
   *   array(array(a,b), array(1,2)) will translate into
751
   *   array(a,1), array(a,2), array(b,1), array(b,2)
752
   */
753
  protected static function multiple_combine($properties) {
754
    $combinations = array();
755
    // Get first key, value. We need to make sure the array pointer is reset.
756
    $value = reset($properties);
757
    $key = key($properties);
758
    array_shift($properties);
759
    $values = is_array($value) ? $value : array($value);
760
    foreach ($values as $value) {
761
      if ($properties) {
762
        foreach (self::multiple_combine($properties) as $merge) {
763
          $combinations[] = array_merge(array($key => $value), $merge);
764
        }
765
      }
766
      else {
767
        $combinations[] = array($key => $value);
768
      }
769
    }
770
    return $combinations;
771
  }
772

    
773
  /**
774
   * Get multiple translations with search conditions.
775
   *
776
   * @param $translations
777
   *   Array of translation objects as loaded from the db.
778
   * @param $langcode
779
   *   Language code, array of language codes or * to search all translations.
780
   *
781
   * @return array
782
   *   Array of i18n string objects.
783
   */
784
  protected function multiple_translation_build($translations, $langcode) {
785
    $strings = array();
786
    foreach ($translations as $translation) {
787
      // The string object may be already in list
788
      if (isset($strings[$translation->context])) {
789
        $string = $strings[$translation->context];
790
      }
791
      else {
792
        $string = $this->build_string($translation->context);
793
        $string->set_properties($translation);
794
        $strings[$string->context] = $string;
795
      }
796
      // If this is a translation we set it there too
797
      if ($translation->language && $translation->translation) {
798
        $string->set_translation($translation);
799
      }
800
      elseif ($langcode) {
801
        // This may only happen when we have a source string but not translation.
802
        $string->set_translation(FALSE, $langcode);
803
      }
804
    }
805
    return $strings;
806
  }
807

    
808
  /**
809
   * Load multiple translations from db
810
   *
811
   * @todo Optimize when we've already got the source object
812
   *
813
   * @param $conditions
814
   *   Array of field values to use as query conditions.
815
   * @param $langcode
816
   *   Language code to search.
817
   * @param $index
818
   *   Field to use as index for the result.
819
   * @return array
820
   *   Array of string objects with translation set.
821
   */
822
  protected function multiple_translation_load($conditions, $langcode) {
823
    $conditions += array(
824
      'language' => $langcode,
825
      'textgroup' => $this->textgroup
826
    );
827
    // We may be querying all translations at the same time or just one language.
828
    // The language field needs some special treatment though.
829
    $query = db_select('i18n_string', 's')->fields('s');
830
    $query->leftJoin('locales_target', 't', 's.lid = t.lid');
831
    $query->fields('t', array('translation', 'language', 'i18n_status'));
832
    foreach ($conditions as $field => $value) {
833
      // Single array value, reduce array
834
      if (is_array($value) && count($value) == 1) {
835
        $value = reset($value);
836
      }
837
      if ($value === '*') {
838
        continue;
839
      }
840
      elseif ($field == 'language') {
841
        $query->condition('t.language', $value);
842
      }
843
      else {
844
        $query->condition('s.' . $field, $value);
845
      }
846
    }
847
    return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
848
  }
849

    
850
  /**
851
   * Search multiple translations with key combinations.
852
   *
853
   * Each $context field may be a single value, an array of values or '*'.
854
   * Example:
855
   * 	array('term', array(1,2), '*')
856
   * This will be mapped into the following conditions (provided language code is 'es')
857
   *  array('type' => 'term', 'objectid' => array(1,2), 'property' => '*', 'language' => 'es')
858
   * And will result in these combinations to search for
859
   *  array('type' => 'term', 'objectid' => 1, 'property' => '*', 'language' => 'es')
860
   *  array('type' => 'term', 'objectid' => 2, 'property' => '*', 'language' => 'es')
861
   *
862
   * @param $context array
863
   *   Array with String context conditions.
864
   *
865
   * @return
866
   *   Array of translation objects indexed by context.
867
   */
868
  public function multiple_translation_search($context, $langcode) {
869
    // First, build conditions and identify the variable field.
870
    $keys = array('type', 'objectid', 'property');
871
    $conditions = array_combine($keys, $context) + array('language' => $langcode);
872
    // Find existing searches in cache, compile remaining ones.
873
    $translations = $search = array();
874
    foreach ($this->multiple_combine($conditions) as $combination) {
875
      $cached = $this->multiple_cache_get($combination);
876
      if (isset($cached)) {
877
        // Cache hit. Merge and remove value from search.
878
        $translations += $cached;
879
      }
880
      else {
881
        // Not in cache, add to search conditions skipping duplicated values.
882
        // As array_merge_recursive() has some bug in PHP 5.2, http://drupal.org/node/1244598
883
        // we use our simplified version here, instead of $search = array_merge_recursive($search, $combination);
884
        foreach ($combination as $key => $value) {
885
          if (!isset($search[$key]) || !in_array($value, $search[$key], TRUE)) {
886
            $search[$key][] = $value;
887
          }
888
        }
889
      }
890
    }
891
    // If we've got any search values left, find translations.
892
    if ($search) {
893
      // Load translations for conditions and set them to the cache
894
      $loaded = $this->multiple_translation_load($search, $langcode);
895
      if ($loaded) {
896
        $translations += $loaded;
897
      }
898
      // Set cache for each of the multiple search keys.
899
      foreach ($this->multiple_combine($search) as $combination) {
900
        $list = $loaded ? $this->string_filter($loaded, $combination) : array();
901
        $this->multiple_cache_set($combination, $list);
902
      }
903
    }
904
    return $translations;
905
  }
906

    
907
  /**
908
   * Set multiple cache.
909
   *
910
   * @param $context
911
   *   String context with language property at the end.
912
   * @param $strings
913
   *   Array of strings (may be empty) to cache.
914
   */
915
  protected function multiple_cache_set($context, $strings) {
916
    $cache_key = implode(':', $context);
917
    $this->cache_multiple[$cache_key] = $strings;
918
  }
919

    
920
  /**
921
   * Get strings from multiple cache.
922
   *
923
   * @param $context array
924
   *   String context as array with language property at the end.
925
   *
926
   * @return mixed
927
   *   Array of strings (may be empty) if we've got a cache hit.
928
   *   Null otherwise.
929
   */
930
  protected function multiple_cache_get($context) {
931
    $cache_key = implode(':', $context);
932
    if (isset($this->cache_multiple[$cache_key])) {
933
      return $this->cache_multiple[$cache_key];
934
    }
935
    else {
936
      // Now we try more generic keys. For instance, if we are searching 'term:1:*'
937
      // we may try too 'term:*:*' and filter out the results.
938
      foreach ($context as $key => $value) {
939
        if ($value != '*') {
940
          $try = array_merge($context, array($key => '*'));
941
          $cache_key = implode(':', $try);
942
          if (isset($this->cache_multiple[$cache_key])) {
943
            // As we've found some more generic key, we need to filter using original conditions.
944
            $strings = $this->string_filter($this->cache_multiple[$cache_key], $context);
945
            return $strings;
946
          }
947
        }
948
      }
949
      // If we've reached here, we didn't find any cache match.
950
      return NULL;
951
    }
952
  }
953

    
954
  /**
955
   * Translate array of source strings
956
   *
957
   * @param $context
958
   *   Context array with placeholders (*)
959
   * @param $strings
960
   *   Optional array of source strings indexed by the placeholder property
961
   *
962
   * @return array
963
   *   Array of string objects (with translation) indexed by the placeholder field
964
   */
965
  public function multiple_translate($context, $strings = array(), $options = array()) {
966
    // First, build conditions and identify the variable field
967
    $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
968
    $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
969
    // If we've got keyed source strings set the array of keys on the placeholder field
970
    // or if not, remove that condition so we search all strings with that keys.
971
    foreach ($search as $field => $value) {
972
      if ($value === '*') {
973
        $property = $field;
974
        if ($strings) {
975
          $search[$field] = array_keys($strings);
976
        }
977
      }
978
    }
979
    // Now we'll add the language code to conditions and get the translations indexed by the property field
980
    $result = $this->multiple_translation_search($search, $langcode);
981
    // Remap translations using property field. If we've got strings it is important that they are in the same order.
982
    $translations = $strings;
983
    foreach ($result as $key => $i18nstring) {
984
      $translations[$i18nstring->$property] = $i18nstring;
985
    }
986
    // Set strings as source or create
987
    foreach ($strings as $key => $source) {
988
      if (isset($translations[$key]) && is_object($translations[$key])) {
989
        $translations[$key]->set_string($source);
990
      }
991
      else {
992
        // Not found any string for this property, create it to map in the response
993
        // But make sure we set this language's translation to FALSE so we don't search again
994
        $newcontext = $context;
995
        $newcontext[$property] = $key;
996
        $translations[$key] = $this->build_string($newcontext)
997
          ->set_string($source)
998
          ->set_translation(FALSE, $langcode);
999
      }
1000
    }
1001
    return $translations;
1002
  }
1003

    
1004
  /**
1005
   * Update string translation, only if source exists.
1006
   *
1007
   * @param $context
1008
   *   String context as array
1009
   * @param $langcode
1010
   *   Language code to create the translation for
1011
   * @param $translation
1012
   *   String translation for this language
1013
   */
1014
  function update_translation($context, $langcode, $translation) {
1015
    $i18nstring = $this->build_string($context);
1016
    if ($source = $i18nstring->get_source()) {
1017
      $source->set_translation($translation, $langcode);
1018
      $this->save_translation($source, $langcode);
1019
      return $source;
1020
    }
1021
  }
1022

    
1023
  /**
1024
   * Recheck strings after update
1025
   */
1026
  public function update_check() {
1027
    // Find strings in locales_source that have no data in i18n_string
1028
    $query = db_select('locales_source', 'l')
1029
    ->fields('l')
1030
    ->condition('l.textgroup', $this->textgroup);
1031
    $alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
1032
    $query->isNull('s.lid');
1033
    foreach ($query->execute()->fetchAll() as $string) {
1034
      $i18nstring = $this->build_string($string->context, $string->source);
1035
      $this->save_string($i18nstring);
1036
    }
1037
  }
1038

    
1039
}
1040

    
1041
/**
1042
 * String object wrapper
1043
 */
1044
class i18n_string_object_wrapper extends i18n_object_wrapper {
1045
  // Text group object
1046
  protected $textgroup;
1047
  // Properties for translation
1048
  protected $properties;
1049

    
1050
  /**
1051
   * Get object strings for translation
1052
   *
1053
    * This will return a simple array of string objects, indexed by full string name.
1054
    *
1055
    * @param $options
1056
    *   Array with processing options.
1057
    *   - 'empty', whether to return empty strings, defaults to FALSE.
1058
   */
1059
  public function get_strings($options = array()) {
1060
    $options += array('empty' => FALSE);
1061
    $strings = array();
1062
    foreach ($this->get_properties() as $textgroup => $textgroup_list) {
1063
      foreach ($textgroup_list as $type => $type_list) {
1064
        foreach ($type_list as $object_id => $object_list) {
1065
          foreach ($object_list as $key => $string) {
1066
            if ($options['empty'] || !empty($string['string'])) {
1067
              // Build string object, that will trigger static caches everywhere.
1068
              $i18nstring = i18n_string_textgroup($textgroup)
1069
                ->build_string(array($type, $object_id, $key))
1070
                ->set_string($string);
1071
              $strings[$i18nstring->get_name()] = $i18nstring;
1072
            }
1073
          }
1074
        }
1075
      }
1076
    }
1077
    return $strings;
1078
  }
1079
  /**
1080
   * Get object translatable properties
1081
   *
1082
   * This will return a big array indexed by textgroup, object type, object id and string key.
1083
   * Each element is an array with string information, and may have these properties:
1084
   * - 'string', the string itself, will be NULL if the object doesn't have that string
1085
   * - 'format', string format when needed
1086
   * - 'title', string readable name
1087
   */
1088
  public function get_properties() {
1089
    if (!isset($this->properties)) {
1090
      $this->properties = $this->build_properties();
1091
      // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
1092
      drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object);
1093

    
1094
    }
1095
    return $this->properties;
1096
  }
1097

    
1098
  /**
1099
   * Build properties from object.
1100
   */
1101
  protected function build_properties() {
1102
    list($string_type, $object_id) = $this->get_string_context();
1103
    $object_keys = array(
1104
      $this->get_textgroup(),
1105
      $string_type,
1106
      $object_id,
1107
    );
1108
    $strings = array();
1109
    foreach ($this->get_string_info('properties', array()) as $field => $info) {
1110
      $info = is_array($info) ? $info : array('title' => $info);
1111
      $field_name = isset($info['field']) ? $info['field'] : $field;
1112
      $value = $this->get_field($field_name);
1113
      $strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
1114
        'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
1115
        'title' => $info['title'],
1116
        'format' => isset($info['format']) ? $this->get_field($info['format']) : NULL,
1117
        'name' => array_merge($object_keys, array($field)),
1118
      );
1119
    }
1120
    return $strings;
1121
  }
1122

    
1123
  /**
1124
   * Get string context
1125
   */
1126
  public function get_string_context() {
1127
    return array($this->get_string_info('type'), $this->get_key());
1128
  }
1129

    
1130
  /**
1131
   * Get translate path for object
1132
   *
1133
   * @param $langcode
1134
   * 	 Language code if we want ti for a specific language
1135
   */
1136
  public function get_translate_path($langcode = NULL) {
1137
    $replacements = array('%i18n_language' => $langcode ? $langcode : '');
1138
    if ($path = $this->get_string_info('translate path')) {
1139
      return $this->path_replace($path, $replacements);
1140
    }
1141
    elseif ($path = $this->get_info('translate tab')) {
1142
      // If we've got a translate tab path, we just add language to it
1143
      return $this->path_replace($path . '/%i18n_language', $replacements);
1144
    }
1145
  }
1146

    
1147
  /**
1148
   * Translation mode for object
1149
   */
1150
  public function get_translate_mode() {
1151
    return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
1152
  }
1153

    
1154
  /**
1155
   * Get textgroup name
1156
   */
1157
  public function get_textgroup() {
1158
    return $this->get_string_info('textgroup');
1159
  }
1160

    
1161
  /**
1162
   * Get textgroup object
1163
   */
1164
  protected function textgroup() {
1165
    if (!isset($this->textgroup)) {
1166
      $this->textgroup = i18n_string_textgroup($this->get_textgroup());
1167
    }
1168
    return $this->textgroup;
1169
  }
1170

    
1171
  /**
1172
   * Translate object.
1173
   *
1174
   * Translations are cached so it runs only once per language.
1175
   *
1176
   * @return object/array
1177
   *   A clone of the object with its properties translated.
1178
   */
1179
  public function translate($langcode, $options = array()) {
1180
    // We may have it already translated. As objects are statically cached, translations are too.
1181
    if (!isset($this->translations[$langcode])) {
1182
      $this->translations[$langcode] = $this->translate_object($langcode, $options);
1183
    }
1184
    return $this->translations[$langcode];
1185
  }
1186

    
1187
  /**
1188
   * Translate access (localize strings)
1189
   */
1190
  protected function localize_access() {
1191
    // We could check also whether the object has strings to translate:
1192
    //   && $this->get_strings(array('empty' => TRUE))
1193
    // However it may be better to display the 'No available strings' message
1194
    // for the user to have a clue of what's going on. See i18n_string_translate_page_object()
1195
    return user_access('translate interface') && user_access('translate user-defined strings');
1196
  }
1197

    
1198
  /**
1199
   * Translate all properties for object.
1200
   *
1201
   * On top of object strings we search for all textgroup:type:objectid:* properties
1202
   *
1203
   * @param $langcode
1204
   *   A clone of the object or array
1205
   */
1206
  protected function translate_object($langcode, $options) {
1207
    // Clone object or array so we don't affect the original one.
1208
    $object = is_object($this->object) ? clone $this->object : $this->object;
1209
    // Get object strings for translatable properties.
1210
    if ($strings = $this->get_strings()) {
1211
      // We preload some of the property translations with a single query.
1212
      if ($context = $this->get_translate_context($langcode, $options)) {
1213
        $found = $this->textgroup()->multiple_translation_search($context, $langcode);
1214
      }
1215
      // Replace all strings in object.
1216
      foreach ($strings as $i18nstring) {
1217
        $this->translate_field($object, $i18nstring, $langcode, $options);
1218
      }
1219
    }
1220
    return $object;
1221
  }
1222

    
1223
  /**
1224
   * Context to be pre-loaded before translation.
1225
   */
1226
  protected function get_translate_context($langcode, $options) {
1227
    // One-query translation of all textgroup:type:objectid:* properties
1228
    $context = $this->get_string_context();
1229
    $context[] = '*';
1230
    return $context;
1231
  }
1232

    
1233
  /**
1234
   * Translate object property.
1235
   *
1236
   * Mot often, this is a direct field set, but sometimes fields may have different formats.
1237
   */
1238
  protected function translate_field(&$object, $i18nstring, $langcode, $options) {
1239
    $field_name = $i18nstring->property;
1240
    $translation = $i18nstring->format_translation($langcode, $options);
1241
    if (is_object($object)) {
1242
      $object->$field_name = $translation;
1243
    }
1244
    elseif (is_array($object)) {
1245
      $object[$field_name] = $translation;
1246
    }
1247
  }
1248

    
1249
  /**
1250
   * Remove all strings for this object.
1251
   */
1252
  public function strings_remove($options = array()) {
1253
    $result = array();
1254
    foreach ($this->load_strings() as $key => $string) {
1255
      $result[$key] = $string->remove($options);
1256
    }
1257
    return _i18n_string_result_count($result);
1258
  }
1259

    
1260
  /**
1261
   * Update all strings for this object.
1262
   */
1263
  public function strings_update($options = array()) {
1264
    $options += array('empty' => TRUE, 'update' => TRUE);
1265
    $result = array();
1266
    $existing = $this->load_strings();
1267
    // Update object strings
1268
    foreach ($this->get_strings($options) as $key => $string) {
1269
      $result[$key] = $string->update($options);
1270
      unset($existing[$key]);
1271
    }
1272
    // Delete old existing strings.
1273
    foreach ($existing as $key => $string) {
1274
      $result[$key] = $string->remove($options);
1275
    }
1276
    return _i18n_string_result_count($result);
1277
  }
1278

    
1279
  /**
1280
   * Load all existing strings for this object.
1281
   */
1282
  public function load_strings() {
1283
    list($type, $id) = $this->get_string_context();
1284
    return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
1285
  }
1286
}