Newer
Older
Gábor Hojtsy
committed
<?php
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
/**
* @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
Gábor Hojtsy
committed
$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);
Gábor Hojtsy
committed
return $string;
}
/**
Gábor Hojtsy
committed
* 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
* @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
Jose Antonio Reyero del Prado
committed
$status = $this->save_source($i18nstring);
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);
Gábor Hojtsy
committed
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;
Gábor Hojtsy
committed
}
}
return $status;
}
/**
* 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;
Gábor Hojtsy
committed
}
}
elseif (isset($string->lid)) {
// It seems we don't have a source string
$this->string_map[$string->context] = FALSE;
}
}
Gábor Hojtsy
committed
/**
* 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;
}
}
Gábor Hojtsy
committed
/**
* 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);
Gábor Hojtsy
committed
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 {
Gábor Hojtsy
committed
// No source, no translation
$translation = $i18nstring;
$translation->translation = FALSE;
}
$this->cache_set($translation);
}
// Return the full object if we've got a translation
Gábor Hojtsy
committed
return $translation->translation !== FALSE ? $translation : NULL;
}
Gábor Hojtsy
committed
/**
* Load translation from db
Gábor Hojtsy
committed
*
* @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'));
Gábor Hojtsy
committed
$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);
Gábor Hojtsy
committed
}
else {
return $translations;
Gábor Hojtsy
committed
}
}
/**
* 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();
}
Gábor Hojtsy
committed
/**
* 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');
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);
}
}
}
}
Gábor Hojtsy
committed
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()) {
Gábor Hojtsy
committed
$options += array('messages' => FALSE);
$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();
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));
Gábor Hojtsy
committed
}
return SAVED_DELETED;
}
}
/**
* Save / update string object
*
* There seems to be a race condition sometimes so skip errors, #277711
Gábor Hojtsy
committed
*
* @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
Gábor Hojtsy
committed
*
* @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();
Gábor Hojtsy
committed
$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
/**
* 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;
}
Gábor Hojtsy
committed
/**
* Translate string object
Gábor Hojtsy
committed
*
* @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;
}
}
Gábor Hojtsy
committed
/**
* 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()) {
Gábor Hojtsy
committed
$options += array('format' => FALSE, 'messages' => FALSE, 'watchdog' => TRUE);
$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;
Gábor Hojtsy
committed
}
}
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) {
Gábor Hojtsy
committed
$i18nstring = (object) array(
'context' => implode(':', $context),
);
if ($source = $this->get_source($i18nstring)) {
$source->language = $langcode;
$source->translation = $translation;
$this->save_translation($source);
return $source;
}
}
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
/**
* 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];
}
}
Gábor Hojtsy
committed
// 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');
Gábor Hojtsy
committed
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;
Gábor Hojtsy
committed
$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();
Gábor Hojtsy
committed
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)));
}
}
Gábor Hojtsy
committed
/**
* 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);
}
}
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
}
/**
* 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 $translation
Jose Antonio Reyero del Prado
committed
* Srtring object with textgroup, langcode, source, translation, format properties
* @return
* The number of strings updated
*/
Jose Antonio Reyero del Prado
committed
function i18n_string_save_translation($translation) {
include_once 'includes/locale.inc';
$query = db_select('locales_source', 's')
->fields('s', array('lid'))
->fields('i', array('format'))
Jose Antonio Reyero del Prado
committed
->condition('s.source', $translation->source)
->condition('s.textgroup', $translation->textgroup);
$query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
Jose Antonio Reyero del Prado
committed
$result = $query->execute()->fetchAll(PDO::FETCH_OBJ);
$count = 0;
foreach ($result as $source) {
// If we have a format, check format access. Otherwise do regular check.
Jose Antonio Reyero del Prado
committed
if ($source->format ? filter_access($source->format) : locale_string_is_safe($translation->translation)) {
$exists = (bool) db_select('locales_target', 'l')
->fields('l', array('lid'))
->condition('lid', $source->lid)
Jose Antonio Reyero del Prado
committed
->condition('language', $translation->langcode)
->execute()
->fetchColumn();
if (!$exists) {
// No translation in this language.
db_insert('locales_target')
->fields(array(
'lid' => $source->lid,
Jose Antonio Reyero del Prado
committed
'language' => $translation->langcode,
'translation' => $translation->translation,
))
->execute();
}
else {
// Translation exists, overwrite
db_update('locales_target')
Jose Antonio Reyero del Prado
committed
->fields(array('translation' => $translation->translation))
->condition('language', $translation->langcode)
->condition('lid', $source->lid)
->execute();
}
$count ++;
}
}
return $count;
}