Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
/**
* @file
* API for internationalization strings
*/
/**
* Textgroup handler for i18n_string API
*/
class i18n_string_default {
// Text group name
public $textgroup;
// Cached or preloaded translations
public $translations;
// Context to locale id map
protected $string_map;
// String formats
protected $string_format;
/**
* Class constructor
*/
public function __construct($textgroup) {
$this->textgroup = $textgroup;
}
/**
* Build string object
*/
public function build_string($context, $source, $options = array()) {
// Set at least the default language to translate to
$string = new Stdclass();
$string->textgroup = $this->textgroup;
$string->source = $source;
if (is_array($context)) {
$string->context = implode(':', $context);
$parts = $context;
}
else {
$string->context = $context;
// Split the name in four parts, remaining elements will be in the last one
// Now we take out 'textgroup'. Context will be the remaining string
$parts = explode(':', $context);
}
// Location will be the full string name
$string->location = $this->textgroup . ':' . $string->context;
$string->type = array_shift($parts);
$string->objectid = $parts ? array_shift($parts) : '';
$string->objectkey = (int)$string->objectid;
// Ramaining elements glued again with ':'
$string->property = $parts ? implode(':', $parts) : '';
// Get elements from the cache
$this->cache_get($string);
return $string;
}
/**
* Check if text format is allowed for translation
*
* @param $format
* Text format key or NULL if not format (will be allowed)
*/
public static function allowed_format($format = NULL) {
return !$format || in_array($format, i18n_string_allowed_formats());
}
/**
* Add source string to the locale tables for translation.
*
* It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
* Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
*
* This function checks for already existing string without context for this textgroup and updates it accordingly.
* It is intended for backwards compatibility, using already created strings.
*
* @param $i18nstring
* String object
* @param $format
* Text format, for strings that will go through some filter
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
* @return
* Update status.
*/
protected function add_string($i18nstring, $options = array()) {
$options += array('watchdog' => TRUE);
// Default return status if nothing happens
$status = -1;
$source = NULL;
// The string may not be allowed for translation depending on its format.
if (!$this->check_string($i18nstring, $options)) {
// The format may have changed and it's not allowed now, delete the source string
return $this->remove_string($i18nstring, $options);
}
elseif ($source = $this->get_source($i18nstring)) {
$i18nstring->lid = $source->lid;
if ($source->source != $i18nstring->source || $source->location != $i18nstring->location) {
// String has changed, mark translations for update
$status = $this->save_source($source);
db_update('locales_target')
->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))
->condition('lid', $source->lid)
->execute();
}
elseif (empty($source->version)) {
// When refreshing strings, we've done version = 0, update it
$this->save_source($i18nstring);
}
}
else {
// We don't have the source object, create it
$status = $this->save_source($i18nstring);
}
// Make sure we have i18n_string part, create or update
// This will also create the source object if doesn't exist
$this->save_string($i18nstring);
if ($options['watchdog']) {
$params = $this->string_params($i18nstring);
switch ($status) {
case SAVED_UPDATED:
watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $params);
break;
case SAVED_NEW:
watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $params);
break;
}
}
return $status;
}
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* Set string object into cache
*/
protected function cache_set($string) {
if (!empty($string->lid)) {
$this->string_map[$string->context] = $string->lid;
if (isset($string->format)) {
$this->string_format[$string->lid] = $string->format;
}
if (isset($string->translation)) {
$this->translations[$string->language][$string->lid] = $string->translation;
}
}
elseif (isset($string->lid)) {
// It seems we don't have a source string
$this->string_map[$string->context] = FALSE;
}
}
/**
* Get translation from cache
*/
protected function cache_get($string) {
if (!isset($string->lid) && isset($this->string_map[$string->context])) {
$string->lid = $this->string_map[$string->context];
}
if (!empty($string->lid)) {
if (isset($this->string_format[$string->lid])) {
$string->format = $this->string_format[$string->lid];
}
if (!empty($string->language) && isset($this->translations[$string->language][$string->lid])) {
$string->translation = $this->translations[$string->language][$string->lid];
}
}
elseif (isset($string->lid)) {
// We don't have a source string, lid == FALSE
if (!empty($string->language)) {
$string->translation = FALSE;
}
}
}
/**
* Reset cache, needed for tests
*/
public function cache_reset() {
$this->string_map = array();
$this->string_format = array();
$this->translations = array();
/**
* Check if string is ok for translation
*/
protected static function check_string($i18nstring, $options = array()) {
$options += array('messages' => FALSE, 'watchdog' => TRUE);
if (!empty($i18nstring->format) && !self::allowed_format($i18nstring->format)) {
// This format is not allowed, so we remove the string, in this case we produce a warning
$params = self::string_params($i18nstring);
drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $params), 'warning');
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
return FALSE;
}
else {
return TRUE;
}
}
/**
* Get source string provided a string object.
*
* @param $context
* Context string or object.
* @return
* Context object if it exists.
*/
protected function get_source($i18nstring) {
// Search the database using lid if we've got it or textgroup, context otherwise
$query = db_select('locales_source', 's')->fields('s');
if (!empty($i18nstring->lid)) {
$query->condition('s.lid', $i18nstring->lid);
}
else {
$query->condition('s.textgroup', $this->textgroup);
$query->condition('s.context', $i18nstring->context);
}
// Speed up the query, we just need one row
$source = $query->range(0, 1)->execute()->fetchObject();
// Update cached map
$this->string_map[$i18nstring->context] = $source ? $source->lid : FALSE;
return $source;
}
/**
* Get translation from the database. Full object with text format.
*
* This one doesn't return anything if we don't have the full i18n strings data there
* to prevent missing data resulting in missing text formats
*/
protected function get_translation($i18nstring) {
// First, populate available data from the cache
$this->cache_get($i18nstring);
if (isset($i18nstring->translation)) {
// Which doesn't mean we've got a translation, only that we've got the result cached
$translation = $i18nstring;
}
else {
$translation = $this->load_translation($i18nstring);
if ($translation) {
$translation->source = $i18nstring->source;
}
else {
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// No source, no translation
$translation = $i18nstring;
$translation->translation = FALSE;
}
$this->cache_set($translation);
}
// Return the full object if we've got a translation
return $translation->translation !== FALSE ? $translation : NULL;
}
/**
* Load translation from db
*
* @todo Optimize when we've already got the source string
*/
protected static function load_translation($i18nstring) {
$query = self::query_string($i18nstring);
$query->leftJoin('locales_target', 't', 's.lid = t.lid');
$query->fields('t', array('translation', 'i18n_status'));
$query->condition('t.language', $i18nstring->language);
// Speed up the query, we just need one row
if (empty($i18nstring->multiple)) {
$query->range(0, 1);
}
$translations = $query->execute()->fetchAll();
foreach ($translations as $translation) {
$translation->translation = isset($translation->translation) ? $translation->translation : FALSE;
$translation->language = $i18nstring->language;
}
if (empty($i18nstring->multiple)) {
return reset($translations);
}
else {
return $translations;
}
}
/**
* Get string source object
*
* @param $context
* Context string or object.
*
* @return
* - Translation string as object if found.
* - FALSE if no translation
*
*/
protected static function get_string($context) {
return self::query_string($context)->execute()->fetchObject();
}
/**
* Build query for i18n_string table
*/
protected static function query_string($context) {
// Search the database using lid if we've got it or textgroup, context otherwise
$query = db_select('i18n_string', 's')->fields('s');
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
if (!empty($context->lid)) {
$query->condition('s.lid', $context->lid);
}
else {
$query->condition('s.textgroup', $context->textgroup);
if (empty($context->multiple)) {
$query->condition('s.context', $context->context);
}
else {
// Query multiple strings
foreach (array('type', 'objectid', 'property') as $field) {
if (!empty($context->$field)) {
$query->condition('s.' . $field, $context->$field);
}
}
}
}
return $query;
}
/**
* Remove source and translations for user defined string.
*
* Though for most strings the 'name' or 'string id' uniquely identifies that string,
* there are some exceptions (like profile categories) for which we need to use the
* source string itself as a search key.
*
* @param $context
* Textgroup and location glued with ':'.
* @param $string
* Optional source string (string in default language).
*/
public function remove($context, $string = NULL, $options = array()) {
$options += array('messages' => TRUE);
$i18nstring = $this->build_string($context, $string, $options);
$status = $this->remove_string($i18nstring, $options);
if ($options['messages'] && $status === SAVED_DELETED) {
drupal_set_message(t('Deleted string %location for textgroup %textgroup: %string', $this->string_params($i18nstring)));
}
return $this;
}
/**
* Remove string object.
*/
public function remove_string($i18nstring, $options = array()) {
$options += array('watchdog' => TRUE);
if ($source = $this->get_source($i18nstring)) {
db_delete('locales_target')->condition('lid', $source->lid)->execute();
db_delete('i18n_string')->condition('lid', $source->lid)->execute();
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
db_delete('locales_source')->condition('lid', $source->lid)->execute();
if ($options['watchdog']) {
watchdog('i18n_string', 'Deleted string %location for textgroup %textgroup: %string', $this->string_params($i18nstring));
}
return SAVED_DELETED;
}
}
/**
* Save / update string object
*
* There seems to be a race condition sometimes so skip errors, #277711
*
* @param $string
* Full string object to be saved
* @param $source
* Source string object
*/
protected function save_string($string, $update = FALSE) {
if (empty($string->lid)) {
if ($source = $this->get_source($string)) {
$string->lid = $source->lid;
}
else {
// Create source string so we get an lid
$this->save_source($string);
}
}
if (!isset($string->objectkey)) {
$string->objectkey = (int)$string->objectid;
}
if (!isset($string->format)) {
}
$status = db_merge('i18n_string')
->key(array('lid' => $string->lid))
->fields(array(
'textgroup' => $string->textgroup,
'context' => $string->context,
'objectid' => $string->objectid,
'type' => $string->type,
'property' => $string->property,
'objectindex' => $string->objectkey,
'format' => $string->format,
))
->execute();
return $status;
}
/**
* Save translation to the db
*
* @param $string
* Full string object with translation data (language, translation)
*/
protected function save_translation($string) {
db_merge('locales_target')
->key(array('lid' => $string->lid, 'language' => $string->language))
->fields(array('translation' => $string->translation))
->execute();
$this->cache_set($string);
}
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
/**
* Save source string (create / update)
*/
protected static function save_source($source) {
if (empty($source->version)) {
$source->version = 1;
}
return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
}
/**
* Get message parameters from context and string.
*/
protected static function string_params($context) {
return array(
'%location' => $context->location,
'%textgroup' => $context->textgroup,
'%string' => isset($context->source) ? $context->source : t('[empty string]'),
);
}
/**
* Translate source string
*/
public function translate($context, $string, $options = array()) {
$i18nstring = $this->build_string($context, $string, $options);
return $this->translate_string($i18nstring, $options);
}
/**
* Translate array of source strings
*/
public function multiple_translate($context, $strings, $options = array()) {
$i18nstring = $this->build_string($context, NULL, $options);
// Set the array of keys on the placeholder field
foreach (array('type', 'objectid', 'property') as $field) {
if ($i18nstring->$field === '*') {
$i18nstring->$field = array_keys($strings);
$property = $field;
}
}
$i18nstring->language = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
$translations = $this->multiple_get_translation($i18nstring);
// Remap translations using index field
$result = $strings;
foreach ($translations as $translation) {
$index = $translation->$property;
$translation->source = $strings[$index];
$result[$index] = $translation;
unset($strings[$index]);
}
// Fill in remaining strings for consistency, l10n_client, etc..
foreach ($strings as $index => $string) {
$translation = clone $i18nstring;
$translation->$property = $index;
$translation->source = $strings[$index];
$result[$index] = $translation;
}
return $result;
}
/**
* Get multiple translations with the available key
*/
public function multiple_get_translation($i18nstring) {
$i18nstring->multiple = TRUE;
$translations = $this->load_translation($i18nstring);
foreach ($translations as $index => $translation) {
$this->cache_set($translation);
if ($translation->translation === FALSE) {
unset($translations[$index]);
}
}
return $translations;
}
/**
* Translate string object
*
* @param $i18nstring
* String object
* @param $options
* Array with aditional options
*/
protected function translate_string($i18nstring, $options = array()) {
$i18nstring->language = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
// Search for existing translation (result will be cached in this function call)
if ($translation = $this->get_translation($i18nstring)) {
return $translation;
}
else {
return $i18nstring;
}
}
/**
* Translate object properties
*/
public function translate_object($type, $object, $options = array()) {
$info = i18n_object_info($type);
$key = $info['key'];
$properties = array_keys($info['string translation']['properties']);
foreach ($properties as $field) {
if (!empty($object->$field)) {
$strings[$field] = $object->$field;
}
}
if (!empty($strings)) {
$context = array($info['string translation']['type'], $object->$key, '*');
$translations = $this->multiple_translate($context, $strings, $options);
foreach ($translation as $field => $value) {
$object->$field = i18n_string_format($value, $options);
}
}
return $object;
}
/**
* Update / create translation source for user defined strings.
*
* @param $name
* Textgroup and location glued with ':'.
* @param $string
* Source string in default language. Default language may or may not be English.
* @param $options
* Array with additional options:
* - 'format', String format if the string has text format
* - 'messages', Whether to print out status messages
*/
public function update($context, $string, $options = array()) {
$options += array('format' => FALSE, 'messages' => TRUE, 'watchdog' => TRUE);
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
$i18nstring = $this->build_string($context, $string, $options);
$i18nstring->format = $options['format'];
if (!$this->check_string($i18nstring, $options)) {
$this->remove($context, $string, $options);
$status = SAVED_DELETED;
}
else {
$status = $this->update_string($i18nstring, $options);
}
if ($options['messages']) {
$params = $this->string_params($i18nstring);
switch ($status) {
case SAVED_UPDATED:
drupal_set_message(t('Updated string %location for textgroup %textgroup: %string', $params));
break;
case SAVED_NEW:
drupal_set_message(t('Created string %location for text group %textgroup: %string', $params));
break;
}
}
return $this;
}
/**
* Update / create / remove string.
*
* @param $name
* String context.
* @pram $string
* New value of string for update/create. May be empty for removing.
* @param $format
* Text format, that must have been checked against allowed formats for translation
* @return status
* SAVED_UPDATED | SAVED_NEW | SAVED_DELETED
*/
protected function update_string($i18nstring, $options = array()) {
if (!empty($i18nstring->source)) {
$status = $this->add_string($i18nstring, $options);
}
else {
$status = $this->remove_string($i18nstring, $options);
}
return $status;
}
/**
* Update string translation.
*/
function update_translation($context, $langcode, $translation) {
if ($source = $this->get_source($context, $translation)) {
$source->language = $langcode;
$source->translation = $translation;
$this->save_translation($source);
return $source;
}
}
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
/**
* Update object properties
*/
public function update_object($type, $object, $options = array()) {
$info = i18n_object_info($type);
$key = $info['key'];
foreach ($info['string translation']['properties'] as $field => $property) {
if (isset($object->$field)) {
$context = array($info['string translation']['type'], $object->$key, $field);
$field_options = is_array($property) && !empty($property['format']) ? array('format' => $object->{$property['format']}) : array();
$this->update($context, $object->$field, $field_options + $options);
}
}
}
/**
* Update context for strings.
*
* As some string locations depend on configurable values, the field needs sometimes to be updated
* without losing existing translations. I.e:
* - profile fields indexed by field name.
* - content types indexted by low level content type name.
*
* Example:
* 'profile:field:oldfield:*' -> 'profile:field:newfield:*'
*/
public static function update_context($oldname, $newname) {
// Get context replacing '*' with empty string.
$oldcontext = explode(':', $oldname);
$newcontext = explode(':', $newname);
/*
i18n_string_context(str_replace('*', '', $oldname));
$newcontext = i18n_string_context(str_replace('*', '', $newname));
*/
// Get location with placeholders.
foreach (array('textgroup', 'type', 'objectid', 'property') as $index => $field) {
if ($oldcontext[$index] != $newcontext[$index]) {
$replace[$field] = $newcontext[$index];
}
}
// Query and replace if there are any fields. It is possible that under some circumstances fields are the same
if (!empty($replace)) {
$textgroup = array_shift($oldcontext);
$context = str_replace('*', '%', implode(':', $oldcontext));
$count = 0;
$query = db_select('i18n_string', 's')
->fields('s')
->condition('s.textgroup', $textgroup)
->condition('s.context', $context, 'LIKE');
foreach ($query->execute()->fetchAll() as $source) {
foreach ($replace as $field => $value) {
$source->$field = $value;
}
// Recalculate location, context, objectindex
$source->context = $source->type . ':' . $source->objectid . ':' . $source->property;
$source->location = $source->textgroup . ':' . $source->context;
$source->objectindex = (int)$source->objectid;
// Update source string.
$update = array(
'textgroup' => $source->textgroup,
'context' => $source->context,
);
db_update('locales_source')
->fields($update + array('location' => $source->location))
->condition('lid', $source->lid)
->execute();
// Update object data.
db_update('i18n_string')
->fields($update + array(
'type' => $source->type,
'objectid' => $source->objectid,
'property' => $source->property,
'objectindex' => $source->objectindex,
))
->condition('lid', $source->lid)
->execute();
$count++;
}
drupal_set_message(t('Updated @count string names from %oldname to %newname.', array('@count' => $count, '%oldname' => $oldname, '%newname' => $newname)));
}
}
/**
* Recheck strings after update
*/
public function update_check() {
// Find strings in locales_source that have no data in i18n_string
$query = db_select('locales_source', 'l')
->fields('l')
->condition('l.textgroup', $this->textgroup);
$alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
$query->condition('s.lid', NULL);
foreach ($query->execute()->fetchAll() as $string) {
list($string->type, $string->objectid, $string->property) = explode(':', $string->context);
$this->save_string($string);
}
}
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
}
/**
* Menu callback. Saves a string translation coming as POST data.
*/
function i18n_string_l10n_client_save_string() {
global $user, $language;
if (user_access('use on-page translation')) {
$textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default';
// Default textgroup will be handled by l10n_client module
if ($textgroup == 'default') {
l10n_client_save_string();
}
elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
$translation = new Stdclass();
$translation->language = $language->language;
$translation->source = $_POST['source'];
$translation->translation = $_POST['target'];
$translation->textgroup = $textgroup;
i18n_string_save_translation($translation);
}
}
}
/**
* Import translation for a given textgroup.
*
* @TODO Check string format properly
*
* This will update multiple strings if there are duplicated ones
*
* @param $langcode
* Language code to import string into.
* @param $source
* Source string.
* @param $translation
* Translation to language specified in $langcode.
* @param $plid
* Optional plural ID to use.
* @param $plural
* Optional plural value to use.
* @return
* The number of strings updated
*/
function i18n_string_save_translation($context) {
include_once 'includes/locale.inc';
$query = db_select('locales_source', 's')
->fields('s', array('lid'))
->fields('i', array('format'))
->condition('s.source', $context->source)
->condition('s.textgroup', $context->textgroup);
$query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
$result->execute()->fetchAll(PDO::FETCH_OBJ);
$count = 0;
foreach ($result as $source) {
// If we have a format, check format access. Otherwise do regular check.
if ($source->format ? filter_access($source->format) : locale_string_is_safe($translation)) {
$exists = (bool) db_select('locales_target', 'l')
->fields('l', array('lid'))
->condition('lid', $source->lid)
->condition('language', $langcode)
->execute()
->fetchColumn();
if (!$exists) {
// No translation in this language.
db_insert('locales_target')
->fields(array(
'lid' => $source->lid,
'language' => $langcode,
'translation' => $translation,
))
->execute();
}
else {
// Translation exists, overwrite
db_update('locales_target')
->fields(array('translation' => $translation))
->condition('language', $langcode)
->condition('lid', $source->lid)
->execute();
}
$count ++;
}
}
return $count;
}