12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502 |
- <?php
- /**
- * @file
- * API for internationalization strings
- */
- /**
- * String object that contains source and translations.
- *
- * Note all database operations must go through textgroup object so we can switch storage at some point.
- */
- class i18n_string_object {
- // Updated source string
- public $string;
- // Properties from locale source
- public $lid;
- public $source;
- public $textgroup;
- public $location;
- public $context;
- public $version;
- // Properties from i18n_tring
- public $type;
- public $objectid;
- public $property;
- public $objectkey;
- public $format;
- // Properties from metadata
- public $title;
- // Array of translations to multiple languages
- public $translations = array();
- // Textgroup object
- protected $_textgroup;
- /**
- * Class constructor
- */
- public function __construct($data = NULL) {
- if ($data) {
- $this->set_properties($data);
- }
- // Attempt to re-build the data from the persistent cache.
- $this->rebuild_from_cache($data);
- }
- /**
- * Rebuild the object data based on the persistent cache.
- *
- * Since the textgroup defines if a string is cacheable or not the caching
- * of the string objects happens in the textgroup handler itself.
- *
- * @see i18n_string_textgroup_cached::__destruct()
- */
- protected function rebuild_from_cache($data = NULL) {
- // Check if we've the required information to repopulate the cache and do so
- // if possible.
- $meta_data_exist = isset($this->textgroup) && isset($this->type) && isset($this->objectid) && isset($this->property);
- if ($meta_data_exist && ($cache = cache_get($this->get_cid())) && !empty($cache->data)) {
- // Re-spawn the cached data.
- // @TODO do we need a array_diff to ensure we don't overwrite the data
- // provided by the $data parameter?
- $this->set_properties($cache->data);
- }
- }
- /**
- * Reset cache, needed for tests.
- */
- public function cache_reset() {
- $this->translations = array();
- // Ensure a possible persistent cache of this object is cleared too.
- cache_clear_all($this->get_cid(), 'cache', TRUE);
- }
- /**
- * Returns the caching id for this object.
- *
- * @return string
- * The caching id.
- */
- public function get_cid() {
- return 'i18n:string:obj:' . $this->get_name();
- }
- /**
- * Get message parameters from context and string.
- */
- public function get_args() {
- return array(
- '%location' => $this->location,
- '%textgroup' => $this->textgroup,
- '%string' => ($string = $this->get_string()) ? $string : t('[empty string]'),
- );
- }
- /**
- * Set context properties
- */
- public function set_context($context) {
- $parts = is_array($context) ? $context : explode(':', $context);
- $this->context = is_array($context) ? implode(':', $context) : $context;
- // Location will be the full string name
- $this->location = $this->textgroup . ':' . $this->context;
- $this->type = array_shift($parts);
- $this->objectid = $parts ? array_shift($parts) : '';
- $this->objectkey = (int)$this->objectid;
- // Remaining elements glued again with ':'
- $this->property = $parts ? implode(':', $parts) : '';
- // Attempt to re-build the other data from the persistent cache.
- $this->rebuild_from_cache();
- return $this;
- }
- /**
- * Get string name including textgroup and context
- */
- public function get_name() {
- return $this->textgroup . ':' . $this->type . ':' . $this->objectid . ':' . $this->property;
- }
- /**
- * Get source string
- */
- public function get_string() {
- if (isset($this->string)) {
- return $this->string;
- }
- elseif (isset($this->source)) {
- return $this->source;
- }
- elseif ($this->textgroup()->debug) {
- return empty($this->lid) ? t('[Source not found]') : t('[String not found]');
- }
- else {
- return '';
- }
- }
- /**
- * Set source string
- *
- * @param $string
- * Plain string or array with 'string', 'format', etc...
- */
- public function set_string($string) {
- if (is_array($string)) {
- $this->string = isset($string['string']) ? $string['string'] : NULL;
- if (isset($string['format'])) {
- $this->format = $string['format'];
- }
- if (isset($string['title'])) {
- $this->title = $string['title'];
- }
- }
- else {
- $this->string = $string;
- }
- return $this;
- }
- /**
- * Get string title.
- */
- public function get_title() {
- return isset($this->title) ? $this->title : t('String');
- }
- /**
- * Get translation to language from string object
- */
- public function get_translation($langcode) {
- if (!isset($this->translations[$langcode])) {
- $translation = $this->textgroup()->load_translation($this, $langcode);
- if ($translation && isset($translation->translation)) {
- $this->set_translation($translation, $langcode);
- }
- else {
- // No source, no translation
- $this->translations[$langcode] = FALSE;
- }
- }
- // Which doesn't mean we've got a translation, only that we've got the result cached
- return $this->translations[$langcode];
- }
- /**
- * Set translation for language
- *
- * @param $translation
- * Translation object (from database) or string
- */
- public function set_translation($translation, $langcode = NULL) {
- if (is_object($translation)) {
- $langcode = $langcode ? $langcode : $translation->language;
- $string = isset($translation->translation) ? $translation->translation : FALSE;
- $this->set_properties($translation);
- }
- else {
- $string = $translation;
- }
- $this->translations[$langcode] = $string;
- return $this;
- }
- /**
- * Format the resulting translation or the default string applying callbacks
- *
- * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
- */
- public function format_translation($langcode, $options = array()) {
- $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug);
- if ($translation = $this->get_translation($langcode)) {
- $string = $translation;
- if (isset($options['filter'])) {
- $string = call_user_func($options['filter'], $string);
- }
- }
- else {
- // Get default source string if no translation.
- $string = $this->get_string();
- $options['sanitize'] = !empty($options['sanitize default']);
- }
- if (!empty($this->format)) {
- $options += array('format' => $this->format);
- }
- // Add debug information if enabled
- if ($options['debug']) {
- $info = array($langcode, $this->textgroup, $this->context);
- if (!empty($this->format)) {
- $info[] = $this->format;
- }
- $options += array('suffix' => '');
- $options['suffix'] .= ' [' . implode(':', $info) . ']';
- }
- // Finally, apply options, filters, callback, etc...
- return i18n_string_format($string, $options);
- }
- /**
- * Get source string provided a string object.
- *
- * @return
- * String object if source exists.
- */
- public function get_source() {
- // If already searched and not found we don't have a source,
- if (isset($this->lid) && !$this->lid) {
- return NULL;
- }
- elseif (!isset($this->lid) || !isset($this->source)) {
- // We may have lid from loading a translation but not loaded the source yet.
- if ($source = $this->textgroup()->load_source($this)) {
- // Set properties but don't override existing ones
- $this->set_properties($source, FALSE, FALSE);
- if (!isset($this->string)) {
- $this->string = $source->source;
- }
- return $this;
- }
- else {
- $this->lid = FALSE;
- return NULL;
- }
- }
- else {
- return $this;
- }
- }
- /**
- * Set properties from object or array
- *
- * @param $properties
- * Obejct or array of properties
- * @param $set_null
- * Whether to set null properties too
- * @param $override
- * Whether to set properties that are already set in this object
- */
- public function set_properties($properties, $set_null = TRUE, $override = TRUE) {
- foreach ((array)$properties as $field => $value) {
- if (property_exists($this, $field) && ($set_null || isset($value)) && ($override || !isset($this->$field))) {
- $this->$field = $value;
- }
- }
- return $this;
- }
- /**
- * Access textgroup object
- */
- protected function textgroup() {
- if (!isset($this->_textgroup)) {
- $this->_textgroup = i18n_string_textgroup($this->textgroup);
- }
- return $this->_textgroup;
- }
- /**
- * Update this string.
- */
- public function update($options = array()) {
- return $this->textgroup()->string_update($this, $options);
- }
- /**
- * Delete this string.
- */
- public function remove($options = array()) {
- return $this->textgroup()->string_remove($this, $options);
- }
- /**
- * Check whether there is any problem for the user to translate a this string.
- *
- * @param $account
- * Optional user account, defaults to current user.
- *
- * @return
- * None if the user has access to translate the string.
- * Error message if the user cannot translate that string.
- */
- public function check_translate_access($account = NULL) {
- return i18n_string_translate_check_string($this, $account);
- }
- }
- /**
- * Textgroup handler for i18n_string API
- */
- class i18n_string_textgroup_default {
- // Text group name
- public $textgroup;
- // Debug flag, set to true to print out more information.
- public $debug;
- // Cached or preloaded string objects
- public $strings = array();
- // Multiple translations search map
- protected $cache_multiple = array();
- /**
- * Class constructor.
- *
- * There are to hidden variables to produce debugging information:
- * - 'i18n_string_debug', generic for all text groups.
- * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
- */
- public function __construct($textgroup) {
- $this->textgroup = $textgroup;
- $this->debug = variable_get('i18n_string_debug', FALSE) || variable_get('i18n_string_debug_' . $textgroup, FALSE);
- }
- /**
- * Build string object
- *
- * @param $context
- * Context array or string
- * @param $string string
- * Current value for string source
- */
- public function build_string($context, $string = NULL) {
- // First try to locate string on cache
- $context = is_array($context) ? implode(':', $context) : $context;
- if ($cached = $this->cache_get($context)) {
- $i18nstring = $cached;
- }
- else {
- $i18nstring = new i18n_string_object();
- $i18nstring->textgroup = $this->textgroup;
- $i18nstring->set_context($context);
- $this->cache_set($context, $i18nstring);
- }
- if (isset($string)) {
- $i18nstring->set_string($string);
- }
- return $i18nstring;
- }
- /**
- * 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 string_add($i18nstring, $options = array()) {
- $options += array('watchdog' => TRUE);
- // Default return status if nothing happens
- $status = -1;
- $source = NULL;
- $location = $i18nstring->location;
- // The string may not be allowed for translation depending on its format.
- if (!$this->string_check($i18nstring, $options)) {
- // The format may have changed and it's not allowed now, delete the source string
- return $this->string_remove($i18nstring, $options);
- }
- elseif ($source = $i18nstring->get_source()) {
- if ($source->source != $i18nstring->string || $source->location != $location) {
- $i18nstring->location = $location;
- // String has changed, mark translations for update
- $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);
- if ($options['watchdog']) {
- switch ($status) {
- case SAVED_UPDATED:
- watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
- break;
- case SAVED_NEW:
- watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
- break;
- }
- }
- return $status;
- }
- /**
- * Check if string is ok for translation
- */
- protected static function string_check($i18nstring, $options = array()) {
- $options += array('messages' => FALSE, 'watchdog' => TRUE);
- 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
- drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
- return FALSE;
- }
- else {
- return TRUE;
- }
- }
- /**
- * Filter array of strings
- *
- * @param array $string_list
- * Array of strings to be filtered.
- * @param array $filter
- * Array of name value conditions.
- *
- * @return array
- * Strings from $string_list that match the filter conditions.
- */
- protected static function string_filter($string_list, $filter) {
- // Remove 'language' and '*' conditions.
- if (isset($filter['language'])) {
- unset($filter['language']);
- }
- while ($field = array_search('*', $filter)) {
- unset($filter[$field]);
- }
- foreach ($string_list as $key => $string) {
- foreach ($filter as $field => $value) {
- if ($string->$field != $value) {
- unset($string_list[$key]);
- break;
- }
- }
- }
- return $string_list;
- }
- /**
- * Build query for i18n_string table
- */
- protected static function string_query($context, $multiple = FALSE) {
- // 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 (!$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 string object.
- *
- * @return
- * SAVED_DELETED | FALSE (If the operation failed because no source)
- */
- public function string_remove($i18nstring, $options = array()) {
- $options += array('watchdog' => TRUE, 'messages' => $this->debug);
- if ($source = $i18nstring->get_source()) {
- 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();
- $this->cache_set($source->context, NULL);
- if ($options['watchdog']) {
- watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
- }
- if ($options['messages']) {
- drupal_set_message(t('Deleted string %location for text group %textgroup: %string', $i18nstring->get_args()));
- }
- return SAVED_DELETED;
- }
- else {
- if ($options['messages']) {
- drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
- }
- return FALSE;
- }
- }
- /**
- * Translate string object
- *
- * @param $i18nstring
- * String object
- * @param $options
- * Array with aditional options
- */
- protected function string_translate($i18nstring, $options = array()) {
- $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
- // Search for existing translation (result will be cached in this function call)
- $i18nstring->get_translation($langcode);
- return $i18nstring;
- }
- /**
- * 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
- * @param $options
- * Processing options, the ones used here are:
- * - 'watchdog', whether to produce watchdog messages.
- * - 'messages', whether to produce user messages.
- * - 'check', whether to check string format and then update/delete if not allowed.
- * @return status
- * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED | FALSE (If the string is to be removed but has no source)
- */
- public function string_update($i18nstring, $options = array()) {
- $options += array('watchdog' => TRUE, 'messages' => $this->debug, 'check' => TRUE);
- if ((!$options['check'] || $this->string_check($i18nstring, $options)) && $i18nstring->get_string()) {
- // String is ok, has a value so we store it into the database.
- $status = $this->string_add($i18nstring, $options);
- }
- elseif ($i18nstring->get_source()) {
- // Just remove it if we already had a source created before.
- $status = $this->string_remove($i18nstring, $options);
- }
- else {
- // String didn't pass validation or we have an empty string but was not stored anyway.
- $status = FALSE;
- }
- if ($options['messages']) {
- switch ($status) {
- case SAVED_UPDATED:
- drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
- break;
- case SAVED_NEW:
- drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
- break;
- }
- }
- if ($options['watchdog']) {
- switch ($status) {
- case SAVED_UPDATED:
- watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
- break;
- case SAVED_NEW:
- watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
- break;
- }
- }
- return $status;
- }
- /**
- * Set string object into cache
- */
- protected function cache_set($context, $string) {
- $this->strings[$context] = $string;
- }
- /**
- * Get translation from cache
- */
- protected function cache_get($context) {
- return isset($this->strings[$context]) ? $this->strings[$context] : NULL;
- }
- /**
- * Reset cache, needed for tests
- */
- public function cache_reset() {
- $this->strings = array();
- $this->string_format = array();
- // Reset the persistent caches.
- cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
- // Reset the complete string object cache too.
- cache_clear_all('i18n:string:obj:', 'cache', TRUE);
- }
- /**
- * Load multiple strings.
- *
- * @return array
- * List of strings indexed by full string name.
- */
- public function load_strings($conditions = array()) {
- // Add textgroup condition and load all
- $conditions['textgroup'] = $this->textgroup;
- $list = array();
- foreach (i18n_string_load_multiple($conditions) as $string) {
- $list[$string->get_name()] = $string;
- $this->cache_set($string->context, $string);
- }
- return $list;
- }
- /**
- * Load string source from db
- */
- public static function load_source($i18nstring) {
- // Search the database using lid if we've got it or textgroup, context otherwise
- $query = db_select('locales_source', 's')->fields('s');
- $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', $i18nstring->textgroup);
- $query->condition('s.context', $i18nstring->context);
- }
- // Speed up the query, we just need one row
- return $query->range(0, 1)->execute()->fetchObject();
- }
- /**
- * Load translation from db
- *
- * @todo Optimize when we've already got the source string
- */
- public static function load_translation($i18nstring, $langcode) {
- // Search the database using lid if we've got it or textgroup, context otherwise
- if (!empty($i18nstring->lid)) {
- // We've already got lid, we just need translation data
- $query = db_select('locales_target', 't');
- $query->condition('t.lid', $i18nstring->lid);
- }
- else {
- // Still don't have lid, load string properties too
- $query = db_select('i18n_string', 's')->fields('s');
- $query->leftJoin('locales_target', 't', 's.lid = t.lid');
- $query->condition('s.textgroup', $i18nstring->textgroup);
- $query->condition('s.context', $i18nstring->context);
- }
- // Add translation fields
- $query->fields('t', array('translation', 'i18n_status'));
- $query->condition('t.language', $langcode);
- // Speed up the query, we just need one row
- $query->range(0, 1);
- return $query->execute()->fetchObject();
- }
- /**
- * 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 (!$string->get_source()) {
- // 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)) {
- $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, $langcode) {
- db_merge('locales_target')
- ->key(array('lid' => $string->lid, 'language' => $langcode))
- ->fields(array('translation' => $string->get_translation($langcode)))
- ->execute();
- }
- /**
- * Save source string (create / update)
- */
- protected static function save_source($source) {
- if (isset($source->string)) {
- $source->source = $source->string;
- }
- if (empty($source->version)) {
- $source->version = 1;
- }
- return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
- }
- /**
- * 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 context_remove($context, $string = NULL, $options = array()) {
- $options += array('messages' => $this->debug);
- $i18nstring = $this->build_string($context, $string);
- $status = $this->string_remove($i18nstring, $options);
- return $this;
- }
- /**
- * Translate source string
- */
- public function context_translate($context, $string, $options = array()) {
- $i18nstring = $this->build_string($context, $string);
- return $this->string_translate($i18nstring, $options);
- }
- /**
- * 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.
- * - 'check', whether to check string format and then update/delete if not allowed.
- */
- public function context_update($context, $string, $options = array()) {
- $options += array('format' => FALSE, 'messages' => $this->debug, 'watchdog' => TRUE, 'check' => TRUE);
- $i18nstring = $this->build_string($context, $string);
- $i18nstring->format = $options['format'];
- $this->string_update($i18nstring, $options);
- return $this;
- }
- /**
- * Build combinations of an array of arrays respecting keys.
- *
- * Example:
- * array(array(a,b), array(1,2)) will translate into
- * array(a,1), array(a,2), array(b,1), array(b,2)
- */
- protected static function multiple_combine($properties) {
- $combinations = array();
- // Get first key, value. We need to make sure the array pointer is reset.
- $value = reset($properties);
- $key = key($properties);
- array_shift($properties);
- $values = is_array($value) ? $value : array($value);
- foreach ($values as $value) {
- if ($properties) {
- foreach (self::multiple_combine($properties) as $merge) {
- $combinations[] = array_merge(array($key => $value), $merge);
- }
- }
- else {
- $combinations[] = array($key => $value);
- }
- }
- return $combinations;
- }
- /**
- * Get multiple translations with search conditions.
- *
- * @param $translations
- * Array of translation objects as loaded from the db.
- * @param $langcode
- * Language code, array of language codes or * to search all translations.
- *
- * @return array
- * Array of i18n string objects.
- */
- protected function multiple_translation_build($translations, $langcode) {
- $strings = array();
- foreach ($translations as $translation) {
- // The string object may be already in list
- if (isset($strings[$translation->context])) {
- $string = $strings[$translation->context];
- }
- else {
- $string = $this->build_string($translation->context);
- $string->set_properties($translation);
- $strings[$string->context] = $string;
- }
- // If this is a translation we set it there too
- if ($translation->language && $translation->translation) {
- $string->set_translation($translation);
- }
- elseif ($langcode) {
- // This may only happen when we have a source string but not translation.
- $string->set_translation(FALSE, $langcode);
- }
- }
- return $strings;
- }
- /**
- * Load multiple translations from db
- *
- * @todo Optimize when we've already got the source object
- *
- * @param $conditions
- * Array of field values to use as query conditions.
- * @param $langcode
- * Language code to search.
- * @param $index
- * Field to use as index for the result.
- * @return array
- * Array of string objects with translation set.
- */
- protected function multiple_translation_load($conditions, $langcode) {
- $conditions += array(
- 'language' => $langcode,
- 'textgroup' => $this->textgroup
- );
- // We may be querying all translations at the same time or just one language.
- // The language field needs some special treatment though.
- $query = db_select('i18n_string', 's')->fields('s');
- $query->leftJoin('locales_target', 't', 's.lid = t.lid');
- $query->fields('t', array('translation', 'language', 'i18n_status'));
- foreach ($conditions as $field => $value) {
- // Single array value, reduce array
- if (is_array($value) && count($value) == 1) {
- $value = reset($value);
- }
- if ($value === '*') {
- continue;
- }
- elseif ($field == 'language') {
- $query->condition('t.language', $value);
- }
- else {
- $query->condition('s.' . $field, $value);
- }
- }
- return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
- }
- /**
- * Search multiple translations with key combinations.
- *
- * Each $context field may be a single value, an array of values or '*'.
- * Example:
- * array('term', array(1,2), '*')
- * This will be mapped into the following conditions (provided language code is 'es')
- * array('type' => 'term', 'objectid' => array(1,2), 'property' => '*', 'language' => 'es')
- * And will result in these combinations to search for
- * array('type' => 'term', 'objectid' => 1, 'property' => '*', 'language' => 'es')
- * array('type' => 'term', 'objectid' => 2, 'property' => '*', 'language' => 'es')
- *
- * @param $context array
- * Array with String context conditions.
- *
- * @return
- * Array of translation objects indexed by context.
- */
- public function multiple_translation_search($context, $langcode) {
- // First, build conditions and identify the variable field.
- $keys = array('type', 'objectid', 'property');
- $conditions = array_combine($keys, $context) + array('language' => $langcode);
- // Find existing searches in cache, compile remaining ones.
- $translations = $search = array();
- foreach ($this->multiple_combine($conditions) as $combination) {
- $cached = $this->multiple_cache_get($combination);
- if (isset($cached)) {
- // Cache hit. Merge and remove value from search.
- $translations += $cached;
- }
- else {
- // Not in cache, add to search conditions skipping duplicated values.
- // As array_merge_recursive() has some bug in PHP 5.2, http://drupal.org/node/1244598
- // we use our simplified version here, instead of $search = array_merge_recursive($search, $combination);
- foreach ($combination as $key => $value) {
- if (!isset($search[$key]) || !in_array($value, $search[$key], TRUE)) {
- $search[$key][] = $value;
- }
- }
- }
- }
- // If we've got any search values left, find translations.
- if ($search) {
- // Load translations for conditions and set them to the cache
- $loaded = $this->multiple_translation_load($search, $langcode);
- if ($loaded) {
- $translations += $loaded;
- }
- // Set cache for each of the multiple search keys.
- foreach ($this->multiple_combine($search) as $combination) {
- $list = $loaded ? $this->string_filter($loaded, $combination) : array();
- $this->multiple_cache_set($combination, $list);
- }
- }
- return $translations;
- }
- /**
- * Set multiple cache.
- *
- * @param $context
- * String context with language property at the end.
- * @param $strings
- * Array of strings (may be empty) to cache.
- */
- protected function multiple_cache_set($context, $strings) {
- $cache_key = implode(':', $context);
- $this->cache_multiple[$cache_key] = $strings;
- }
- /**
- * Get strings from multiple cache.
- *
- * @param $context array
- * String context as array with language property at the end.
- *
- * @return mixed
- * Array of strings (may be empty) if we've got a cache hit.
- * Null otherwise.
- */
- protected function multiple_cache_get($context) {
- $cache_key = implode(':', $context);
- if (isset($this->cache_multiple[$cache_key])) {
- return $this->cache_multiple[$cache_key];
- }
- else {
- // Now we try more generic keys. For instance, if we are searching 'term:1:*'
- // we may try too 'term:*:*' and filter out the results.
- foreach ($context as $key => $value) {
- if ($value != '*') {
- $try = array_merge($context, array($key => '*'));
- $cache_key = implode(':', $try);
- if (isset($this->cache_multiple[$cache_key])) {
- // As we've found some more generic key, we need to filter using original conditions.
- $strings = $this->string_filter($this->cache_multiple[$cache_key], $context);
- return $strings;
- }
- }
- }
- // If we've reached here, we didn't find any cache match.
- return NULL;
- }
- }
- /**
- * Translate array of source strings
- *
- * @param $context
- * Context array with placeholders (*)
- * @param $strings
- * Optional array of source strings indexed by the placeholder property
- *
- * @return array
- * Array of string objects (with translation) indexed by the placeholder field
- */
- public function multiple_translate($context, $strings = array(), $options = array()) {
- // First, build conditions and identify the variable field
- $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
- $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
- // If we've got keyed source strings set the array of keys on the placeholder field
- // or if not, remove that condition so we search all strings with that keys.
- foreach ($search as $field => $value) {
- if ($value === '*') {
- $property = $field;
- if ($strings) {
- $search[$field] = array_keys($strings);
- }
- }
- }
- // Now we'll add the language code to conditions and get the translations indexed by the property field
- $result = $this->multiple_translation_search($search, $langcode);
- // Remap translations using property field. If we've got strings it is important that they are in the same order.
- $translations = $strings;
- foreach ($result as $key => $i18nstring) {
- $translations[$i18nstring->$property] = $i18nstring;
- }
- // Set strings as source or create
- foreach ($strings as $key => $source) {
- if (isset($translations[$key]) && is_object($translations[$key])) {
- $translations[$key]->set_string($source);
- }
- else {
- // Not found any string for this property, create it to map in the response
- // But make sure we set this language's translation to FALSE so we don't search again
- $newcontext = $context;
- $newcontext[$property] = $key;
- $translations[$key] = $this->build_string($newcontext)
- ->set_string($source)
- ->set_translation(FALSE, $langcode);
- }
- }
- return $translations;
- }
- /**
- * 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) {
- $i18nstring = $this->build_string($context);
- if ($source = $i18nstring->get_source()) {
- $source->set_translation($translation, $langcode);
- $this->save_translation($source, $langcode);
- return $source;
- }
- }
- /**
- * 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->isNull('s.lid');
- foreach ($query->execute()->fetchAll() as $string) {
- $i18nstring = $this->build_string($string->context, $string->source);
- $this->save_string($i18nstring);
- }
- }
- }
- /**
- * String object wrapper
- */
- class i18n_string_object_wrapper extends i18n_object_wrapper {
- // Text group object
- protected $textgroup;
- // Properties for translation
- protected $properties;
- /**
- * Get object strings for translation
- *
- * This will return a simple array of string objects, indexed by full string name.
- *
- * @param $options
- * Array with processing options.
- * - 'empty', whether to return empty strings, defaults to FALSE.
- */
- public function get_strings($options = array()) {
- $options += array('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 ($options['empty'] || !empty($string['string'])) {
- // Build string object, that will trigger static caches everywhere.
- $i18nstring = i18n_string_textgroup($textgroup)
- ->build_string(array($type, $object_id, $key))
- ->set_string($string);
- $strings[$i18nstring->get_name()] = $i18nstring;
- }
- }
- }
- }
- }
- return $strings;
- }
- /**
- * Get object translatable properties
- *
- * 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() {
- if (!isset($this->properties)) {
- $this->properties = $this->build_properties();
- // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
- drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object);
- }
- return $this->properties;
- }
- /**
- * Build properties from object.
- */
- protected function build_properties() {
- list($string_type, $object_id) = $this->get_string_context();
- $object_keys = array(
- $this->get_textgroup(),
- $string_type,
- $object_id,
- );
- $strings = array();
- foreach ($this->get_string_info('properties', array()) 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);
- $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)),
- );
- }
- return $strings;
- }
- /**
- * Get string context
- */
- public function get_string_context() {
- return array($this->get_string_info('type'), $this->get_key());
- }
- /**
- * Get translate path for object
- *
- * @param $langcode
- * Language code if we want ti for a specific language
- */
- public function get_translate_path($langcode = NULL) {
- $replacements = array('%i18n_language' => $langcode ? $langcode : '');
- if ($path = $this->get_string_info('translate path')) {
- 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 . '/%i18n_language', $replacements);
- }
- }
- /**
- * Translation mode for object
- */
- public function get_translate_mode() {
- return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
- }
- /**
- * Get textgroup name
- */
- public function get_textgroup() {
- return $this->get_string_info('textgroup');
- }
- /**
- * Get textgroup object
- */
- protected function textgroup() {
- if (!isset($this->textgroup)) {
- $this->textgroup = i18n_string_textgroup($this->get_textgroup());
- }
- return $this->textgroup;
- }
- /**
- * Translate object.
- *
- * Translations are cached so it runs only once per language.
- *
- * @return object/array
- * A clone of the object with its properties translated.
- */
- public function translate($langcode, $options = array()) {
- // We may have it already translated. As objects are statically cached, translations are too.
- if (!isset($this->translations[$langcode])) {
- $this->translations[$langcode] = $this->translate_object($langcode, $options);
- }
- return $this->translations[$langcode];
- }
- /**
- * Translate access (localize strings)
- */
- protected function localize_access() {
- // We could check also whether the object has strings to translate:
- // && $this->get_strings(array('empty' => TRUE))
- // However it may be better to display the 'No available strings' message
- // for the user to have a clue of what's going on. See i18n_string_translate_page_object()
- return user_access('translate interface') && user_access('translate user-defined strings');
- }
- /**
- * Translate all properties for object.
- *
- * On top of object strings we search for all textgroup:type:objectid:* properties
- *
- * @param $langcode
- * A clone of the object or array
- */
- protected function translate_object($langcode, $options) {
- // Clone object or array so we don't affect the original one.
- $object = is_object($this->object) ? clone $this->object : $this->object;
- // Get object strings for translatable properties.
- if ($strings = $this->get_strings()) {
- // We preload some of the property translations with a single query.
- if ($context = $this->get_translate_context($langcode, $options)) {
- $found = $this->textgroup()->multiple_translation_search($context, $langcode);
- }
- // Replace all strings in object.
- foreach ($strings as $i18nstring) {
- $this->translate_field($object, $i18nstring, $langcode, $options);
- }
- }
- return $object;
- }
- /**
- * Context to be pre-loaded before translation.
- */
- protected function get_translate_context($langcode, $options) {
- // One-query translation of all textgroup:type:objectid:* properties
- $context = $this->get_string_context();
- $context[] = '*';
- return $context;
- }
- /**
- * Translate object property.
- *
- * Mot often, this is a direct field set, but sometimes fields may have different formats.
- */
- protected function translate_field(&$object, $i18nstring, $langcode, $options) {
- $field_name = $i18nstring->property;
- $translation = $i18nstring->format_translation($langcode, $options);
- if (is_object($object)) {
- $object->$field_name = $translation;
- }
- elseif (is_array($object)) {
- $object[$field_name] = $translation;
- }
- }
- /**
- * Remove all strings for this object.
- */
- public function strings_remove($options = array()) {
- $result = array();
- foreach ($this->load_strings() as $key => $string) {
- $result[$key] = $string->remove($options);
- }
- return _i18n_string_result_count($result);
- }
- /**
- * Update all strings for this object.
- */
- public function strings_update($options = array()) {
- $options += array('empty' => TRUE, 'update' => TRUE);
- $result = array();
- $existing = $this->load_strings();
- // Update object strings
- foreach ($this->get_strings($options) as $key => $string) {
- $result[$key] = $string->update($options);
- unset($existing[$key]);
- }
- // Delete old existing strings.
- foreach ($existing as $key => $string) {
- $result[$key] = $string->remove($options);
- }
- return _i18n_string_result_count($result);
- }
- /**
- * Load all existing strings for this object.
- */
- public function load_strings() {
- list($type, $id) = $this->get_string_context();
- return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
- }
- }
- /**
- * Textgroup handler for i18n_string API which integrated persistent caching.
- */
- class i18n_string_textgroup_cached extends i18n_string_textgroup_default {
- /**
- * Defines the timeout for the persistent caching.
- * @var int
- */
- public $caching_time = CACHE_TEMPORARY;
- /**
- * Extends the existing constructor with a cache handling.
- *
- * @param string $textgroup
- * The name of this textgroup.
- */
- public function __construct($textgroup) {
- parent::__construct($textgroup);
- // Fetch persistent caches, the persistent caches contain only metadata.
- // Those metadata are processed by the related cache_get() methods.
- foreach (array('cache_multiple', 'strings') as $caches_type) {
- if (($cache = cache_get('i18n:string:tgroup:' . $this->textgroup . ':' . $caches_type)) && !empty($cache->data)) {
- $this->{$caches_type} = $cache->data;
- }
- }
- }
- /**
- * Class destructor.
- *
- * Updates the persistent caches for the next usage.
- * This function not only stores the data of the textgroup objects but also
- * of the string objects. That way we ensure that only cacheable string object
- * go into the persistent cache.
- */
- public function __destruct() {
- // Reduce size to cache by removing NULL values.
- $this->strings = array_filter($this->strings);
- $strings_to_cache = array();
- // Store the persistent caches. We just store the metadata the translations
- // are stored by the string object itself. However storing the metadata
- // reduces the number of DB queries executed during runtime.
- $cache_data = array();
- foreach ($this->strings as $context => $i18n_string_object) {
- $cache_data[$context] = $context;
- $strings_to_cache[$context] = $i18n_string_object;
- }
- cache_set('i18n:string:tgroup:' . $this->textgroup . ':strings', $cache_data, 'cache', $this->caching_time);
- $cache_data = array();
- foreach ($this->cache_multiple as $pattern => $strings) {
- foreach ($strings as $context => $i18n_string_object) {
- $cache_data[$pattern][$context] = $context;
- $strings_to_cache[$context] = $i18n_string_object;
- }
- }
- cache_set('i18n:string:tgroup:' . $this->textgroup . ':cache_multiple', $cache_data, 'cache', $this->caching_time);
- // Cache the string objects related to this textgroup.
- // Store only the public visible data into the persistent cache.
- foreach ($strings_to_cache as $i18n_string_object) {
- // If this isn't an object it's an unprocessed cache item and doesn't need
- // to be stored again.
- if (is_object($i18n_string_object)) {
- cache_set($i18n_string_object->get_cid(), get_object_vars($i18n_string_object), 'cache', $this->caching_time);
- }
- }
- }
- /**
- * Reset cache, needed for tests.
- *
- * Takes care of the persistent caches.
- */
- public function cache_reset() {
- // Reset the persistent caches.
- cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
- // Reset the complete string object cache too. This will affect string
- // objects of other textgroups as well.
- cache_clear_all('i18n:string:obj:', 'cache', TRUE);
- return parent::cache_reset();
- }
- /**
- * Get translation from cache.
- *
- * Extends the original handler with persistent caching.
- *
- * @param string $context
- * The context to look out for.
- * @return i18n_string_object|NULL
- * The string object if available or NULL otherwise.
- */
- protected function cache_get($context) {
- if (isset($this->strings[$context])) {
- // If the cache contains a string re-build i18n_string_object.
- if (is_string($this->strings[$context])) {
- $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
- $i8n_string_object->set_context($context);
- $this->strings[$context] = $i8n_string_object;
- }
- // Now run the original handling.
- return parent::cache_get($context);
- }
- return NULL;
- }
- /**
- * Get strings from multiple cache.
- *
- * @param $context array
- * String context as array with language property at the end.
- *
- * @return mixed
- * Array of strings (may be empty) if we've got a cache hit.
- * Null otherwise.
- */
- protected function multiple_cache_get($context) {
- // Ensure the values from the persistent cache are properly re-build.
- $cache_key = implode(':', $context);
- if (isset($this->cache_multiple[$cache_key])) {
- foreach ($this->cache_multiple[$cache_key] as $cached_context) {
- if (is_string($cached_context)) {
- $i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
- $i8n_string_object->set_context($cached_context);
- $this->cache_multiple[$cache_key][$cached_context] = $i8n_string_object;
- }
- }
- }
- else {
- // Now we try more generic keys. For instance, if we are searching 'term:1:*'
- // we may try too 'term:*:*' and filter out the results.
- foreach ($context as $key => $value) {
- if ($value != '*') {
- $try = array_merge($context, array($key => '*'));
- return $this->multiple_cache_get($try);
- }
- }
- }
- return parent::multiple_cache_get($context);
- }
- public function string_update($i18nstring, $options = array()) {
- // Flush persistent cache.
- cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
- return parent::string_update($i18nstring, $options);
- }
- public function string_remove($i18nstring, $options = array()) {
- // Flush persistent cache.
- cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
- return parent::string_remove($i18nstring, $options);
- }
- }
|