Newer
Older
Gábor Hojtsy
committed
<?php
/**
* @file
* API for internationalization strings
*/
/**
* Textgroup handler for i18n_string API
*/
Jose Antonio Reyero del Prado
committed
class i18n_string_textgroup_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;
}
/**
* 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);
Jose Antonio Reyero del Prado
committed
if (!empty($i18nstring->format) && !i18n_string_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');
return FALSE;
}
else {
return TRUE;
}
}
/**
* Get source string provided a string object.
*
* @param $context
Jose Antonio Reyero del Prado
committed
* Context array or string object.
* @return
Jose Antonio Reyero del Prado
committed
* String object if it exists.
*/
Jose Antonio Reyero del Prado
committed
public function get_source($i18nstring) {
if (!is_object($i18nstring)) {
$i18nstring = $this->build_string($i18nstring, NULL);
}
// Search the database using lid if we've got it or textgroup, context otherwise
$query = db_select('locales_source', 's')->fields('s');
Jose Antonio Reyero del Prado
committed
$query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
$query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
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();
Jose Antonio Reyero del Prado
committed
// Update cached map if we've got a context
$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);
}
405
406
407
408
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
/**
* 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;
Jose Antonio Reyero del Prado
committed
$translation->source = is_array($strings[$index]) ? $strings[$index]['string'] : $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;
Jose Antonio Reyero del Prado
committed
$translation->source = is_array($strings[$index]) ? $strings[$index]['string'] : $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
/**
* 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, only if source exists.
*
* @param $context
* String context as array
* @param $langcode
* Language code to create the translation for
* @param $translation
* String translation for this language
*/
function update_translation($context, $langcode, $translation) {
Jose Antonio Reyero del Prado
committed
if ($source = $this->get_source($context)) {
$source->language = $langcode;
$source->translation = $translation;
$this->save_translation($source);
return $source;
}
}
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);
}
}
}
/**
* String object wrapper
*/
Jose Antonio Reyero del Prado
committed
class i18n_string_object_wrapper extends i18n_object_wrapper {
// Text group object
protected $textgroup;
Jose Antonio Reyero del Prado
committed
// Object translations
protected $translations;
/**
* Get object strings for translation
Jose Antonio Reyero del Prado
committed
*
* This will return a simple array of arrays, indexed by string key.
* Each item will have the same elements as in i18n_string_object_properties() and
* - 'name', full string name array (textgroup, object type, object id, string key)
*/
Jose Antonio Reyero del Prado
committed
public function get_strings($include_empty = FALSE) {
$strings = array();
foreach ($this->get_properties() as $textgroup => $textgroup_list) {
foreach ($textgroup_list as $type => $type_list) {
foreach ($type_list as $object_id => $object_list) {
foreach ($object_list as $key => $string) {
if ($include_empty || !empty($string['string'])) {
$strings[$key] = $string + array(
'name' => array($textgroup, $type, $object_id, $key),
);
}
}
}
}
}
return $strings;
}
/**
* Get object translatable properties
Jose Antonio Reyero del Prado
committed
*
* This will return a big array indexed by textgroup, object type, object id and string key.
* Each element is an array with string information, and may have these properties:
* - 'string', the string itself, will be NULL if the object doesn't have that string
* - 'format', string format when needed
* - 'title', string readable name
*/
public function get_properties() {
list($string_type, $object_id) = $this->get_string_context();
Jose Antonio Reyero del Prado
committed
$object_keys = array(
$this->get_textgroup(),
$string_type,
$object_id,
);
Jose Antonio Reyero del Prado
committed
$strings = array();
foreach ($this->get_string_info('properties') as $field => $info) {
$info = is_array($info) ? $info : array('title' => $info);
$field_name = isset($info['field']) ? $info['field'] : $field;
$value = $this->get_field($field_name);
Jose Antonio Reyero del Prado
committed
$strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
'title' => $info['title'],
'format' => isset($info['format']) ? $this->get_field($info['format']) : NULL,
'name' => array_merge($object_keys, array($field)),
);
}
Jose Antonio Reyero del Prado
committed
// Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
drupal_alter('i18n_string_list_' . $this->get_textgroup(), $strings, $this->type, $this->object);
return $strings;
}
/**
* Get object info
*/
public function get_string_info($property) {
return i18n_string_object_info($this->type, $property);
}
/**
* Get string context
*/
public function get_string_context() {
Jose Antonio Reyero del Prado
committed
return array($this->get_string_info('type'), $this->get_key());
}
/**
* Get translate path for object
*
Jose Antonio Reyero del Prado
committed
* @param $langcode
* Language code if we want ti for a specific language
*/
Jose Antonio Reyero del Prado
committed
public function get_translate_path($langcode = NULL) {
$replacements = array('%language' => $langcode ? $langcode : '');
if ($path = $this->get_string_info('translate path')) {
Jose Antonio Reyero del Prado
committed
return $this->path_replace($path, $replacements);
}
elseif ($path = $this->get_info('translate tab')) {
// If we've got a translate tab path, we just add language to it
return $this->path_replace($path . '/%language', $replacements);
}
}
/**
Jose Antonio Reyero del Prado
committed
* Translation mode for object
*/
Jose Antonio Reyero del Prado
committed
public function get_translate_mode() {
return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
}
/**
Jose Antonio Reyero del Prado
committed
* Get textgroup name
*/
Jose Antonio Reyero del Prado
committed
public function get_textgroup() {
return $this->get_string_info('textgroup');
}
/**
* Get textgroup object
*/
protected function textgroup() {
if (!isset($this->textgroup)) {
Jose Antonio Reyero del Prado
committed
$this->textgroup = i18n_string_textgroup($this->get_textgroup());
}
return $this->textgroup;
}
/**
* Translate object properties
*/
public function translate($options = array()) {
Jose Antonio Reyero del Prado
committed
$options['langcode'] = $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
Jose Antonio Reyero del Prado
committed
// We may have it already translated. As objects are statically cached, translations are too.
Jose Antonio Reyero del Prado
committed
if (isset($this->translations[$langcode])) {
return $this->translations[$langcode];
}
else {
$object = is_object($this->object) ? clone $this->object : $this->object;
if ($strings = $this->get_strings()) {
$context = $this->get_string_context();
$context[] = '*';
$translations = $this->textgroup()->multiple_translate($context, $strings, $options);
// Clone real objects, mark translated...
foreach ($translations as $field => $value) {
if (is_object($object)) {
$object->$field = i18n_string_format($value, $options);
}
elseif (is_array($object)) {
$object[$field] = i18n_string_format($value, $options);
}
}
}
Jose Antonio Reyero del Prado
committed
return $this->translations[$langcode] = $object;
}