4 * API for internationalization strings
8 * String object that contains source and translations.
10 * Note all database operations must go through textgroup object so we can switch storage at some point.
12 class i18n_string_object
{
13 // Updated source string
15 // Properties from locale source
22 // Properties from i18n_tring
28 // Properties from metadata
30 // Array of translations to multiple languages
33 protected
$_textgroup;
38 public
function __construct($data = NULL
) {
40 $this->set_properties($data);
44 * Get message parameters from context and string.
46 public
function get_args() {
48 '%location' => $this->location
,
49 '%textgroup' => $this->textgroup
,
50 '%string' => ($string = $this->get_string()) ?
$string : t('[empty string]'),
54 * Set context properties
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) : '';
69 * Get string name including textgroup and context
71 public
function get_name() {
72 return $this->textgroup .
':' .
$this->type .
':' .
$this->objectid .
':' .
$this->property
;
77 public
function get_string() {
78 if (isset($this->string)) {
81 elseif (isset($this->source
)) {
84 elseif ($this->textgroup()->debug
) {
85 return empty($this->lid
) ?
t('[Source not found]') : t('[String not found]');
95 * Plain string or array with 'string', 'format', etc...
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'];
105 $this->string = $string;
107 if (isset($string['title'])) {
108 $this->title
= $string['title'];
115 public
function get_title() {
116 return isset($this->title
) ?
$this->title
: t('String');
119 * Get translation to language from string object
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);
128 // No source, no translation
129 $this->translations
[$langcode] = FALSE
;
132 // Which doesn't mean we've got a translation, only that we've got the result cached
133 return $this->translations
[$langcode];
136 * Set translation for language
138 * @param $translation
139 * Translation object (from database) or string
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);
148 $string = $translation;
150 $this->translations
[$langcode] = $string;
155 * Format the resulting translation or the default string applying callbacks
157 * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
159 public
function format_translation($langcode, $options = array()) {
160 $options += array('langcode' => $langcode, 'sanitize' => TRUE
, 'sanitize default' => FALSE
, '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);
168 // Get default source string if no translation.
169 $string = $this->get_string();
170 $options['sanitize'] = $options['sanitize default'];
172 if (!empty($this->format
)) {
173 $options += array('format' => $this->format
);
175 elseif (empty($translation) && $options['sanitize']) {
176 // We are bout to display the source string without text format, force check_plain.
177 $string = check_plain($string);
179 // Add debug information if enabled
180 if ($options['debug']) {
181 $info = array($langcode, $this->textgroup
, $this->context
);
182 if (!empty($this->format
)) {
183 $info[] = $this->format
;
185 $options += array('suffix' => '');
186 $options['suffix'] .
= ' [' .
implode(':', $info) .
']';
188 // Finally, apply options, filters, callback, etc...
189 return i18n_string_format($string, $options);
193 * Get source string provided a string object.
196 * String object if source exists.
198 public
function get_source() {
199 // If already searched and not found we don't have a source,
200 if (isset($this->lid
) && !$this->lid
) {
203 elseif (!isset($this->lid
) || !isset($this->source
)) {
204 // We may have lid from loading a translation but not loaded the source yet.
205 if ($source = $this->textgroup()->load_source($this)) {
206 $this->set_properties($source, FALSE
);
207 if (!isset($this->string)) {
208 $this->string = $source->source
;
223 * Set properties from object or array
226 * Obejct or array of properties
228 * Whether to set null properties too
230 public
function set_properties($properties, $set_null = TRUE
) {
231 foreach ((array)$properties as
$field => $value) {
232 if (property_exists($this, $field) && ($set_null || isset($value))) {
233 $this->$field = $value;
239 * Access textgroup object
241 protected
function textgroup() {
242 if (!isset($this->_textgroup
)) {
243 $this->_textgroup
= i18n_string_textgroup($this->textgroup
);
245 return $this->_textgroup
;
248 * Update this string.
250 public
function update($options = array()) {
251 return $this->textgroup()->string_update($this, $options);
254 * Delete this string.
256 public
function remove($options = array()) {
257 return $this->textgroup()->string_remove($this, $options);
262 * Textgroup handler for i18n_string API
264 class i18n_string_textgroup_default
{
267 // Debug flag, set to true to print out more information.
269 // Cached or preloaded string objects
271 // Multiple translations search map
272 protected
$cache_multiple;
277 * There are to hidden variables to produce debugging information:
278 * - 'i18n_string_debug', generic for all text groups.
279 * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
281 public
function __construct($textgroup) {
282 $this->textgroup
= $textgroup;
283 $this->debug
= variable_get('i18n_string_debug', FALSE
) || variable_get('i18n_string_debug_' .
$textgroup, FALSE
);
286 * Build string object
289 * Context array or string
290 * @param $string string
291 * Current value for string source
293 public
function build_string($context, $string = NULL
) {
294 // First try to locate string on cache
295 $context = is_array($context) ?
implode(':', $context) : $context;
296 if ($cached = $this->cache_get($context)) {
297 $i18nstring = $cached;
300 $i18nstring = new
i18n_string_object();
301 $i18nstring->textgroup
= $this->textgroup
;
302 $i18nstring->set_context($context);
303 $this->cache_set($context, $i18nstring);
305 if (isset($string)) {
306 $i18nstring->set_string($string);
311 * Add source string to the locale tables for translation.
313 * It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
314 * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
316 * This function checks for already existing string without context for this textgroup and updates it accordingly.
317 * It is intended for backwards compatibility, using already created strings.
322 * Text format, for strings that will go through some filter
326 protected
function string_add($i18nstring, $options = array()) {
327 $options += array('watchdog' => TRUE
);
328 // Default return status if nothing happens
331 $location = $i18nstring->location
;
332 // The string may not be allowed for translation depending on its format.
333 if (!$this->string_check($i18nstring, $options)) {
334 // The format may have changed and it's not allowed now, delete the source string
335 return $this->string_remove($i18nstring, $options);
337 elseif ($source = $i18nstring->get_source()) {
338 if ($source->source
!= $i18nstring->string || $source->location
!= $location) {
339 $i18nstring->location
= $location;
340 // String has changed, mark translations for update
341 $status = $this->save_source($i18nstring);
342 db_update('locales_target')
343 ->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE
))
344 ->condition('lid', $source->lid
)
347 elseif (empty($source->version
)) {
348 // When refreshing strings, we've done version = 0, update it
349 $this->save_source($i18nstring);
353 // We don't have the source object, create it
354 $status = $this->save_source($i18nstring);
356 // Make sure we have i18n_string part, create or update
357 // This will also create the source object if doesn't exist
358 $this->save_string($i18nstring);
360 if ($options['watchdog']) {
363 watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
366 watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
374 * Check if string is ok for translation
376 protected static
function string_check($i18nstring, $options = array()) {
377 $options += array('messages' => FALSE
, 'watchdog' => TRUE
);
378 if (!empty($i18nstring->format
) && !i18n_string_allowed_format($i18nstring->format
)) {
379 // This format is not allowed, so we remove the string, in this case we produce a warning
380 drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
389 * Filter array of strings
392 * Array of name value conditions.
394 protected static
function string_filter($string_list, $filter) {
395 // Remove 'language' and '*' conditions.
396 if (isset($filter['language'])) {
397 unset($filter['language']);
399 while ($field = array_search('*', $filter)) {
400 unset($filter[$field]);
402 foreach ($string_list as
$key => $string) {
403 foreach ($filter as
$field => $value) {
404 if ($string->$field != $value) {
405 unset($string_list[$key]);
414 * Build query for i18n_string table
416 protected static
function string_query($context, $multiple = FALSE
) {
417 // Search the database using lid if we've got it or textgroup, context otherwise
418 $query = db_select('i18n_string', 's')->fields('s');
419 if (!empty($context->lid
)) {
420 $query->condition('s.lid', $context->lid
);
423 $query->condition('s.textgroup', $context->textgroup
);
425 $query->condition('s.context', $context->context
);
428 // Query multiple strings
429 foreach (array('type', 'objectid', 'property') as
$field) {
430 if (!empty($context->$field)) {
431 $query->condition('s.' .
$field, $context->$field);
440 * Remove string object.
442 public
function string_remove($i18nstring, $options = array()) {
443 $options += array('watchdog' => TRUE
, 'messages' => $this->debug
);
444 if ($source = $i18nstring->get_source()) {
445 db_delete('locales_target')->condition('lid', $source->lid
)->execute();
446 db_delete('i18n_string')->condition('lid', $source->lid
)->execute();
447 db_delete('locales_source')->condition('lid', $source->lid
)->execute();
448 $this->cache_set($source->context
, NULL
);
449 if ($options['watchdog']) {
450 watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
452 if ($options['messages']) {
453 drupal_set_message(t('Deleted string %location for tex tgroup %textgroup: %string', $i18nstring->get_args()));
455 return SAVED_DELETED
;
458 if($options['messages']) {
459 drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
466 * Translate string object
471 * Array with aditional options
473 protected
function string_translate($i18nstring, $options = array()) {
474 $langcode = isset($options['langcode']) ?
$options['langcode'] : i18n_langcode();
475 // Search for existing translation (result will be cached in this function call)
476 $i18nstring->get_translation($langcode);
481 * Update / create / remove string.
486 * New value of string for update/create. May be empty for removing.
488 * Text format, that must have been checked against allowed formats for translation
490 * Processing options, the ones used here are:
491 * - 'watchdog', whether to produce watchdog messages.
492 * - 'messages', whether to produce user messages.
493 * - 'check', whether to check string format and then update/delete if not allowed.
495 * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED
497 public
function string_update($i18nstring, $options = array()) {
498 $options += array('watchdog' => TRUE
, 'messages' => FALSE
, 'check' => TRUE
);
499 if (!$options['check'] || !$this->string_check($i18nstring, $options)) {
500 $status = $this->string_remove($i18nstring, $options);
502 elseif ($i18nstring->get_string()) {
503 $status = $this->string_add($i18nstring, $options);
506 $status = $this->string_remove($i18nstring, $options);
508 if ($options['messages']) {
511 drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
514 drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
518 if ($options['watchdog']) {
521 watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
524 watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
532 * Set string object into cache
534 protected
function cache_set($context, $string) {
535 $this->strings
[$context] = $string;
539 * Get translation from cache
541 protected
function cache_get($context) {
542 return isset($this->strings
[$context]) ?
$this->strings
[$context] : NULL
;
546 * Reset cache, needed for tests
548 public
function cache_reset() {
549 $this->strings
= array();
550 $this->string_format
= array();
551 $this->translations
= array();
555 * Load string source from db
557 public static
function load_source($i18nstring) {
558 // Search the database using lid if we've got it or textgroup, context otherwise
559 $query = db_select('locales_source', 's')->fields('s');
560 $query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
561 $query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
562 if (!empty($i18nstring->lid
)) {
563 $query->condition('s.lid', $i18nstring->lid
);
566 $query->condition('s.textgroup', $i18nstring->textgroup
);
567 $query->condition('s.context', $i18nstring->context
);
569 // Speed up the query, we just need one row
570 return $query->range(0, 1)->execute()->fetchObject();
574 * Load translation from db
576 * @todo Optimize when we've already got the source string
578 public static
function load_translation($i18nstring, $langcode) {
579 // Search the database using lid if we've got it or textgroup, context otherwise
580 if (!empty($i18nstring->lid
)) {
581 // We've alreay got lid, we just need translation data
582 $query = db_select('locales_target', 't');
583 $query->condition('t.lid', $i18nstring->lid
);
586 // Still don't have lid, load string properties too
587 $query = db_select('i18n_string', 's')->fields('s');
588 $query->leftJoin('locales_target', 't', 's.lid = t.lid');
589 $query->condition('s.textgroup', $i18nstring->textgroup
);
590 $query->condition('s.context', $i18nstring->context
);
592 // Add translation fields
593 $query->fields('t', array('translation', 'i18n_status'));
594 $query->condition('t.language', $langcode);
595 // Speed up the query, we just need one row
597 return $query->execute()->fetchObject();
601 * Save / update string object
603 * There seems to be a race condition sometimes so skip errors, #277711
606 * Full string object to be saved
608 * Source string object
610 protected
function save_string($string, $update = FALSE
) {
611 if (!$string->get_source()) {
612 // Create source string so we get an lid
613 $this->save_source($string);
615 if (!isset($string->objectkey
)) {
616 $string->objectkey
= (int)$string->objectid
;
618 if (!isset($string->format
)) {
619 $string->format
= '';
621 $status = db_merge('i18n_string')
622 ->key(array('lid' => $string->lid
))
624 'textgroup' => $string->textgroup
,
625 'context' => $string->context
,
626 'objectid' => $string->objectid
,
627 'type' => $string->type
,
628 'property' => $string->property
,
629 'objectindex' => $string->objectkey
,
630 'format' => $string->format
,
637 * Save translation to the db
640 * Full string object with translation data (language, translation)
642 protected
function save_translation($string, $langcode) {
643 db_merge('locales_target')
644 ->key(array('lid' => $string->lid
, 'language' => $langcode))
645 ->fields(array('translation' => $string->get_translation($langcode)))
650 * Save source string (create / update)
652 protected static
function save_source($source) {
653 if (isset($source->string)) {
654 $source->source
= $source->string;
656 if (empty($source->version
)) {
657 $source->version
= 1;
659 return drupal_write_record('locales_source', $source, !empty($source->lid
) ?
'lid' : array());
663 * Remove source and translations for user defined string.
665 * Though for most strings the 'name' or 'string id' uniquely identifies that string,
666 * there are some exceptions (like profile categories) for which we need to use the
667 * source string itself as a search key.
670 * Textgroup and location glued with ':'.
672 * Optional source string (string in default language).
674 public
function context_remove($context, $string = NULL
, $options = array()) {
675 $options += array('messages' => $this->debug
);
676 $i18nstring = $this->build_string($context, $string);
677 $status = $this->string_remove($i18nstring, $options);
683 * Translate source string
685 public
function context_translate($context, $string, $options = array()) {
686 $i18nstring = $this->build_string($context, $string);
687 return $this->string_translate($i18nstring, $options);
691 * Update / create translation source for user defined strings.
694 * Textgroup and location glued with ':'.
696 * Source string in default language. Default language may or may not be English.
698 * Array with additional options:
699 * - 'format', String format if the string has text format.
700 * - 'messages', Whether to print out status messages.
701 * - 'check', whether to check string format and then update/delete if not allowed.
703 public
function context_update($context, $string, $options = array()) {
704 $options += array('format' => FALSE
, 'messages' => $this->debug
, 'watchdog' => TRUE
, 'check' => TRUE
);
705 $i18nstring = $this->build_string($context, $string);
706 $i18nstring->format
= $options['format'];
707 $this->string_update($i18nstring, $options);
712 * Build combinations of an array of arrays respecting keys.
715 * array(array(a,b), array(1,2)) will translate into
716 * array(a,1), array(a,2), array(b,1), array(b,2)
718 protected static
function multiple_combine($properties) {
719 $combinations = array();
720 // Get first key, value. We need to make sure the array pointer is reset.
721 $value = reset($properties);
722 $key = key($properties);
723 array_shift($properties);
724 $values = is_array($value) ?
$value : array($value);
725 foreach ($values as
$value) {
727 foreach (self
::multiple_combine($properties) as
$merge) {
728 $combinations[] = array_merge(array($key => $value), $merge);
732 $combinations[] = array($key => $value);
735 return $combinations;
739 * Get multiple translations with search conditions.
741 * @param $translations
742 * Array of translation objects as loaded from the db.
744 * Language code, array of language codes or * to search all translations.
747 * Array of i18n string objects.
749 protected
function multiple_translation_build($translations, $langcode) {
751 foreach ($translations as
$translation) {
752 // The string object may be already in list
753 if (isset($strings[$translation->context
])) {
754 $string = $strings[$translation->context
];
757 $string = $this->build_string($translation->context
);
758 $string->set_properties($translation);
759 $strings[$string->context
] = $string;
761 // If this is a translation we set it there too
762 if ($translation->language
&& $translation->translation
) {
763 $string->set_translation($translation);
766 // This may only happen when we have a source string but not translation.
767 $string->set_translation(FALSE
, $langcode);
774 * Load multiple translations from db
776 * @todo Optimize when we've already got the source object
779 * Array of field values to use as query conditions.
781 * Language code to search.
783 * Field to use as index for the result.
785 * Array of string objects with translation set.
787 protected
function multiple_translation_load($conditions, $langcode) {
788 $conditions += array(
789 'language' => $langcode,
790 'textgroup' => $this->textgroup
792 // We may be querying all translations at the same time or just one language.
793 // The language field needs some special treatment though.
794 $query = db_select('i18n_string', 's')->fields('s');
795 $query->leftJoin('locales_target', 't', 's.lid = t.lid');
796 $query->fields('t', array('translation', 'language', 'i18n_status'));
797 foreach ($conditions as
$field => $value) {
798 // Single array value, reduce array
799 if (is_array($value) && count($value) == 1) {
800 $value = reset($value);
802 if ($value === '*') {
805 elseif ($field == 'language') {
806 $query->condition('t.language', $value);
809 $query->condition('s.' .
$field, $value);
812 return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
816 * Search multiple translations with key combinations.
818 * @param $context array()
819 * String context conditions.
820 * Each context field may be a single value, an array of values or '*'
823 * Array of translation objects indexed by context.
825 public
function multiple_translation_search($context, $langcode) {
826 // First, build conditions and identify the variable field.
827 $keys = array('type', 'objectid', 'property');
828 $conditions = array_combine($keys, $context) + array('language' => $langcode);
829 // Find existing searches in cache, compile remaining ones.
830 $translations = $search = array();
831 foreach ($this->multiple_combine($conditions) as
$combination) {
832 $cached = $this->multiple_cache_get($combination);
833 if (isset($cached)) {
834 // Cache hit. Merge and remove value from search.
835 $translations += $cached;
838 // Not in cache, add to search conditions.
839 $search = array_merge_recursive($search, $combination);
842 // If we've got any search values left, find translations.
844 // Prepare conditions for search, de-dupe conditions values.
845 foreach ($search as
$key => $value) {
846 if (is_array($value)) {
847 $search[$key] = array_unique($value);
850 // Load translations for conditions and set them to the cache
851 $loaded = $this->multiple_translation_load($search, $langcode);
853 $translations += $loaded;
855 // Set cache for each of the multiple search keys.
856 foreach ($this->multiple_combine($search) as
$combination) {
857 $list = $loaded ?
$this->string_filter($loaded, $combination) : array();
858 $this->multiple_cache_set($combination, $list);
861 return $translations;
865 * Set multiple cache.
868 * String context with language property at the end.
870 * Array of strings (may be empty) to cache.
872 protected
function multiple_cache_set($context, $strings) {
873 $cache_key = implode(':', $context);
874 $this->cache_multiple
[$cache_key] = $strings;
878 * Get strings from multiple cache.
881 * String context with language property at the end.
884 * Array of strings (may be empty) if we've got a cache hit.
887 protected
function multiple_cache_get($context) {
888 $cache_key = implode(':', $context);
889 if (isset($this->cache_multiple
[$cache_key])) {
890 return $this->cache_multiple
[$cache_key];
893 // Now we try more generic keys. For instance, if we are searching 'term:1:*'
894 // we may try too 'term:*:*' and filter out the results.
895 foreach ($context as
$key => $value) {
897 $try = array_merge($context, array($key => '*'));
898 $cache_key = implode(':', $try);
899 if (isset($this->cache_multiple
[$cache_key])) {
900 // As we've found some more generic key, we need to filter using original conditions.
901 $strings = $this->string_filter($this->cache_multiple
[$cache_key], $context);
906 // If we've reached here, we didn't find any cache match.
912 * Translate array of source strings
915 * Context array with placeholders (*)
917 * Optional array of source strings indexed by the placeholder property
920 * Array of string objects (with translation) indexed by the placeholder field
922 public
function multiple_translate($context, $strings = array(), $options = array()) {
923 // First, build conditions and identify the variable field
924 $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
925 $langcode = isset($options['langcode']) ?
$options['langcode'] : i18n_langcode();
926 // If we've got keyed source strings set the array of keys on the placeholder field
927 // or if not, remove that condition so we search all strings with that keys.
928 foreach ($search as
$field => $value) {
929 if ($value === '*') {
932 $search[$field] = array_keys($strings);
936 // Now we'll add the language code to conditions and get the translations indexed by the property field
937 $result = $this->multiple_translation_search($search, $langcode);
938 // Remap translations using property field. If we've got strings it is important that they are in the same order.
939 $translations = $strings;
940 foreach ($result as
$key => $i18nstring) {
941 $translations[$i18nstring->$property] = $i18nstring;
943 // Set strings as source or create
944 foreach ($strings as
$key => $source) {
945 if (isset($translations[$key]) && is_object($translations[$key])) {
946 $translations[$key]->set_string($source);
949 // Not found any string for this property, create it to map in the response
950 // But make sure we set this language's translation to FALSE so we don't search again
951 $newcontext = $context;
952 $newcontext[$property] = $key;
953 $translations[$key] = $this->build_string($newcontext)
954 ->set_string($source)
955 ->set_translation(FALSE
, $langcode);
958 return $translations;
962 * Update string translation, only if source exists.
965 * String context as array
967 * Language code to create the translation for
968 * @param $translation
969 * String translation for this language
971 function update_translation($context, $langcode, $translation) {
972 $i18nstring = $this->build_string($context);
973 if ($source = $i18nstring->get_source()) {
974 $source->set_translation($translation, $langcode);
975 $this->save_translation($source, $langcode);
981 * Recheck strings after update
983 public
function update_check() {
984 // Find strings in locales_source that have no data in i18n_string
985 $query = db_select('locales_source', 'l')
987 ->condition('l.textgroup', $this->textgroup
);
988 $alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
989 $query->condition('s.lid', NULL
);
990 foreach ($query->execute()->fetchAll() as
$string) {
991 $i18nstring = $this->build_string($string->context
, $string->source
);
992 $this->save_string($i18nstring);
999 * String object wrapper
1001 class i18n_string_object_wrapper
extends i18n_object_wrapper
{
1002 // Text group object
1003 protected
$textgroup;
1004 // Properties for translation
1005 protected
$properties;
1008 * Get object strings for translation
1010 * This will return a simple array of string objects, indexed by full string name.
1013 * Array with processing options.
1014 * - 'empty', whether to return empty strings, defaults to FALSE.
1016 public
function get_strings($options = array()) {
1017 $options += array('empty' => FALSE
);
1019 foreach ($this->get_properties() as
$textgroup => $textgroup_list) {
1020 foreach ($textgroup_list as
$type => $type_list) {
1021 foreach ($type_list as
$object_id => $object_list) {
1022 foreach ($object_list as
$key => $string) {
1023 if ($options['empty'] || !empty($string['string'])) {
1024 // Build string object, that will trigger static caches everywhere.
1025 $i18nstring = i18n_string_textgroup($textgroup)
1026 ->build_string(array($type, $object_id, $key))
1027 ->set_string($string);
1028 $strings[$i18nstring->get_name()] = $i18nstring;
1037 * Get object translatable properties
1039 * This will return a big array indexed by textgroup, object type, object id and string key.
1040 * Each element is an array with string information, and may have these properties:
1041 * - 'string', the string itself, will be NULL if the object doesn't have that string
1042 * - 'format', string format when needed
1043 * - 'title', string readable name
1045 public
function get_properties() {
1046 if (!isset($this->properties
)) {
1047 $this->properties
= $this->build_properties();
1049 return $this->properties
;
1053 * Build properties from object.
1055 protected
function build_properties() {
1056 list($string_type, $object_id) = $this->get_string_context();
1057 $object_keys = array(
1058 $this->get_textgroup(),
1063 foreach ($this->get_string_info('properties', array()) as
$field => $info) {
1064 $info = is_array($info) ?
$info : array('title' => $info);
1065 $field_name = isset($info['field']) ?
$info['field'] : $field;
1066 $value = $this->get_field($field_name);
1067 $strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
1068 'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL
: $value,
1069 'title' => $info['title'],
1070 'format' => isset($info['format']) ?
$this->get_field($info['format']) : NULL
,
1071 'name' => array_merge($object_keys, array($field)),
1074 // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
1075 drupal_alter('i18n_string_list_' .
$this->get_textgroup(), $strings, $this->type
, $this->object);
1080 * Get string context
1082 public
function get_string_context() {
1083 return array($this->get_string_info('type'), $this->get_key());
1087 * Get translate path for object
1090 * Language code if we want ti for a specific language
1092 public
function get_translate_path($langcode = NULL
) {
1093 $replacements = array('%language' => $langcode ?
$langcode : '');
1094 if ($path = $this->get_string_info('translate path')) {
1095 return $this->path_replace($path, $replacements);
1097 elseif ($path = $this->get_info('translate tab')) {
1098 // If we've got a translate tab path, we just add language to it
1099 return $this->path_replace($path .
'/%language', $replacements);
1104 * Translation mode for object
1106 public
function get_translate_mode() {
1107 return !$this->get_langcode() ? I18N_MODE_LOCALIZE
: I18N_MODE_NONE
;
1111 * Get textgroup name
1113 public
function get_textgroup() {
1114 return $this->get_string_info('textgroup');
1118 * Get textgroup object
1120 protected
function textgroup() {
1121 if (!isset($this->textgroup
)) {
1122 $this->textgroup
= i18n_string_textgroup($this->get_textgroup());
1124 return $this->textgroup
;
1130 * Translations are cached so it runs only once per language.
1132 * @return object/array
1133 * A clone of the object with its properties translated.
1135 public
function translate($langcode, $options = array()) {
1136 // We may have it already translated. As objects are statically cached, translations are too.
1137 if (!isset($this->translations
[$langcode])) {
1138 $this->translations
[$langcode] = $this->translate_object($langcode, $options);
1140 return $this->translations
[$langcode];
1144 * Translate access (localize strings)
1146 protected
function localize_access() {
1147 return user_access('translate interface');
1151 * Translate all properties for object.
1153 * On top of object strings we search for all textgroup:type:objectid:* properties
1156 * A clone of the object or array
1158 protected
function translate_object($langcode, $options) {
1159 // Clone object or array so we don't affect the original one.
1160 $object = is_object($this->object) ? clone
$this->object : $this->object;
1161 // Get object strings for translatable properties.
1162 if ($strings = $this->get_strings()) {
1163 // We preload some of the property translations with a single query.
1164 if ($context = $this->get_translate_context($langcode, $options)) {
1165 $found = $this->textgroup()->multiple_translation_search($context, $langcode);
1167 // Replace all strings in object.
1168 foreach ($strings as
$i18nstring) {
1169 $this->translate_field($object, $i18nstring, $langcode, $options);
1176 * Context to be pre-loaded before translation.
1178 protected
function get_translate_context($langcode, $options) {
1179 // One-query translation of all textgroup:type:objectid:* properties
1180 $context = $this->get_string_context();
1186 * Translate object property.
1188 * Mot often, this is a direct field set, but sometimes fields may have different formats.
1190 protected
function translate_field(&$object, $i18nstring, $langcode, $options) {
1191 $field_name = $i18nstring->property
;
1192 $translation = $i18nstring->format_translation($langcode, $options);
1193 if (is_object($object)) {
1194 $object->$field_name = $translation;
1196 elseif (is_array($object)) {
1197 $object[$field_name] = $translation;
1202 * Remove all strings for this object.
1204 public
function strings_remove($options = array()) {
1205 return $this->strings_operation('remove', $options + array('empty' => TRUE
));
1209 * Update all strings for this object.
1211 public
function strings_update($options = array()) {
1212 return $this->strings_operation('update', $options + array('empty' => TRUE
, 'update' => TRUE
));
1216 * Run some operation on all this object's strings.
1219 * Operation to run on all strings: 'update', 'remove'.
1221 * Array of options to pass to the string method.
1224 * Count for each result
1226 protected
function strings_operation($method, $options = array()) {
1228 foreach ($this->get_strings($options) as
$string) {
1229 $status = $string->$method($options);
1230 $result[$status] = isset($result[$status]) ?
$result[$status] +1 : 1;