initMade = false; } /** * Updates language count in system (always is divisible by 5) * */ protected function _queryLanguages() { if ( !$this->initMade ) { $this->languagesIDs = $this->getActualLanguages(); $this->languageCount = max(max($this->languagesIDs), 5); $this->initMade = true; } } /** * Returns language ids, that can be used * * @return Array */ protected function getActualLanguages() { $cache_key = 'actual_language_ids[%LangSerial%]'; $ret = $this->Application->getCache($cache_key); if ( $ret === false ) { $this->Conn->nextQueryCachable = true; $config = $this->Application->getUnitConfig('lang'); $sql = 'SELECT ' . $config->getIDField() . ' FROM ' . $config->getTableName(); $ret = $this->Conn->GetCol($sql); $this->Application->setCache($cache_key, $ret); } return $ret; } /** * Checks if language with specified id is created * * @param int $language_id * @return bool */ protected function LanguageFound($language_id) { return in_array($language_id, $this->languagesIDs) || $language_id <= 5; } /** * Returns list of processable languages * * @return Array */ public function getLanguages() { $cache_key = 'processable_language_ids[%LangSerial%]'; $ret = $this->Application->getCache($cache_key); if ( $ret === false ) { $ret = Array (); $this->_queryLanguages(); for ($language_id = 1; $language_id <= $this->languageCount; $language_id++) { if ( $this->LanguageFound($language_id) ) { $ret[] = $language_id; } } $this->Application->setCache($cache_key, $ret); } return $ret; } function scanTable($mask) { $i = 0; $fields_found = 0; $fields = array_keys($this->curStructure); foreach ($fields as $field_name) { if (preg_match($mask, $field_name)) { $fields_found++; } } return $fields_found; } function readTableStructure($table_name, $refresh = false) { // if ($refresh || !getArrayValue($structure_status, $prefix.'.'.$table_name)) { $this->curStructure = $this->Conn->Query('DESCRIBE '.$table_name, 'Field'); $this->curIndexCount = count($this->Conn->Query('SHOW INDEXES FROM '.$table_name)); // } } /** * Creates missing multilingual fields for all unit configs, registered in system * * @param bool $reread_configs * @return void * @access public */ public function massCreateFields($reread_configs = true) { if ( $reread_configs ) { $this->Application->UnitConfigReader->ReReadConfigs(); } foreach ($this->Application->UnitConfigReader->getPrefixes() as $prefix) { $this->createFields($prefix); } } /** * Creates missing multilanguage fields in table by specified prefix * * @param string $prefix * @param bool $refresh Forces config field structure to be re-read from database * @return void */ function createFields($prefix, $refresh = false) { if ( $refresh && preg_match('/(.*)-cdata$/', $prefix, $regs) ) { // call main item config to clone cdata table $this->Application->UnitConfigReader->loadConfig($regs[1]); $this->Application->UnitConfigReader->runAfterConfigRead($prefix); } $config = $this->Application->getUnitConfig($prefix); $table_name = $config->getTableName(); $this->curFields = $config->getFields(); if ( !($table_name && $this->curFields) || ($table_name && !$this->Conn->TableFound($table_name, kUtil::constOn('IS_INSTALL'))) ) { // invalid config found or prefix not found return ; } $this->_queryLanguages(); $sqls = Array (); $this->readTableStructure($table_name, $refresh); foreach ($this->curFields as $field_name => $field_options) { if ( getArrayValue($field_options, 'formatter') == 'kMultiLanguage' ) { if ( isset($field_options['master_field']) ) { unset($this->curFields[$field_name]); continue; } $this->setSourceField($field_name); if ( $this->languageCount > 0 ) { // `l77_Name` VARCHAR( 255 ) NULL DEFAULT '0'; $field_mask = Array (); $field_mask['name'] = 'l%s_' . $field_name; $field_mask['null'] = getArrayValue($field_options, 'not_null') ? 'NOT NULL' : 'NULL'; if ( $this->curSourceField ) { $default_value = $this->getFieldParam('Default') != 'NULL' ? $this->Conn->qstr($this->getFieldParam('Default')) : $this->getFieldParam('Default'); $field_mask['type'] = $this->getFieldParam('Type'); } else { $default_value = is_null($field_options['default']) ? 'NULL' : $this->Conn->qstr($field_options['default']); $field_mask['type'] = $field_options['db_type']; } $field_mask['default'] = ($field_mask['null'] == 'NOT NULL' && $default_value == 'NULL') ? '' : 'DEFAULT ' . $default_value; if ( strtoupper($field_mask['type']) == 'TEXT' ) { // text fields in mysql doesn't have default value $field_mask = $field_mask['name'] . ' ' . $field_mask['type'] . ' ' . $field_mask['null']; } else { $field_mask = $field_mask['name'] . ' ' . $field_mask['type'] . ' ' . $field_mask['null'] . ' ' . $field_mask['default']; } $alter_sqls = $this->generateAlterSQL($field_mask, 1, $this->languageCount); if ( $alter_sqls ) { $sqls[] = 'ALTER TABLE ' . $table_name . ' ' . $alter_sqls; } } } } foreach ($sqls as $sql_query) { $this->Conn->Query($sql_query); } } /** * Creates missing multilanguage fields in table by specified prefix * * @param string $prefix * @param int $src_language * @param int $dst_language * @return void * @access public */ public function copyMissingData($prefix, $src_language, $dst_language) { $config = $this->Application->getUnitConfig($prefix); $table_name = $config->getTableName(); $this->curFields = $config->getFields(); if ( !($table_name && $this->curFields) || ($table_name && !$this->Conn->TableFound($table_name, kUtil::constOn('IS_INSTALL'))) ) { // invalid config found or prefix not found return ; } foreach ($this->curFields as $field_name => $field_options) { $formatter = isset($field_options['formatter']) ? $field_options['formatter'] : ''; if ( ($formatter == 'kMultiLanguage') && !isset($field_options['master_field']) ) { $sql = 'UPDATE ' . $table_name . ' SET l' . $dst_language . '_' . $field_name . ' = l' . $src_language . '_' . $field_name . ' WHERE l' . $dst_language . '_' . $field_name . ' = "" OR l' . $dst_language . '_' . $field_name . ' IS NULL'; $this->Conn->Query($sql); } } } function deleteField($prefix, $custom_id) { $table_name = $this->Application->getUnitConfig($prefix)->getTableName(); $sql = 'DESCRIBE '.$table_name.' "l%_cust_'.$custom_id.'"'; $fields = $this->Conn->GetCol($sql); $sql = 'ALTER TABLE '.$table_name.' '; $sql_template = 'DROP COLUMN %s, '; foreach ($fields as $field_name) { $sql .= sprintf($sql_template, $field_name); } $this->Conn->Query( substr($sql, 0, -2) ); } /** * Returns parameter requested of current source field * * @param string $param_name * @return string */ function getFieldParam($param_name) { return $this->curStructure[$this->curSourceField][$param_name]; } /** * Detects field name to create other fields from * * @param string $field_name */ function setSourceField($field_name) { $ret = $this->scanTable('/^l[\d]+_'.preg_quote($field_name, '/').'$/'); if (!$ret) { // no multilingual fields at all (but we have such field without language prefix) $original_found = $this->scanTable('/^'.preg_quote($field_name, '$/').'/'); $this->curSourceField = $original_found ? $field_name : false; } else { $this->curSourceField = 'l1_'.$field_name; } } /** * Returns ALTER statement part for adding required fields to table * * @param string $field_mask sql mask for creating field with correct definition (type & size) * @param int $start_index add new fields starting from this index * @param int $create_count create this much new multilingual field translations * @return string */ function generateAlterSQL($field_mask, $start_index, $create_count) { static $single_lang = null; if (!isset($single_lang)) { // if single language mode, then create indexes only on primary columns $table_name = $this->Application->getUnitConfig('lang')->getTableName(); $sql = 'SELECT COUNT(*) FROM '.$table_name.' WHERE Enabled = 1'; // if language count = 0, then assume it's multi language mode $single_lang = $this->Conn->GetOne($sql) == 1; } $ret = ''; $ml_field = preg_replace('/l(.*?)_(.*?) (.*)/', '\\2', $field_mask); $i_count = $start_index + $create_count; while ($start_index < $i_count) { if (isset($this->curStructure['l'.$start_index.'_'.$ml_field]) || (!$this->LanguageFound($start_index)) ) { $start_index++; continue; } $prev_index = $start_index - 1; do { list($prev_field,$type) = explode(' ', sprintf($field_mask, $prev_index) ); } while ($prev_index > 0 && !$this->LanguageFound($prev_index--)); if (substr($prev_field, 0, 3) == 'l0_') { $prev_field = substr($prev_field, 3, strlen($prev_field)); if (!$this->curSourceField) { // get field name before this one $fields = array_keys($this->curFields); // $prev_field = key(end($this->curStructure)); $prev_field = $fields[array_search($prev_field, $fields) - 1]; if (getArrayValue($this->curFields[$prev_field], 'formatter') == 'kMultiLanguage') { $prev_field = 'l'.$this->languageCount.'_'.$prev_field; } } } $field_expression = sprintf($field_mask, $start_index); $ret .= 'ADD COLUMN '.$field_expression.' AFTER `'.$prev_field.'`, '; if ($this->curIndexCount < 32 && ($start_index == $this->Application->GetDefaultLanguageId() || !$single_lang)) { // create index for primary language column + for all others (if multiple languages installed) list($field_name, $field_params) = explode(' ', $field_expression, 2); if ( isset($this->curFields[$ml_field]['index_type']) ) { $index_type = $this->curFields[$ml_field]['index_type']; } else { $index_type = 'string'; } $ret .= $index_type == 'string' ? 'ADD INDEX (`'.$field_name.'` (5) ), ' : 'ADD INDEX (`'.$field_name.'`), '; $this->curIndexCount++; } $start_index++; } return preg_replace('/, $/', ';', $ret); } /** * Returns phrase based on given number * * @param int $number * @param Array $forms * @param bool $allow_editing * @param bool $use_admin * @return string * @access public */ public function getPluralPhrase($number, $forms, $allow_editing = true, $use_admin = false) { // normalize given forms if ( !array_key_exists('phrase3', $forms) ) { $forms['phrase3'] = $forms['phrase2']; } if ( !array_key_exists('phrase4', $forms) ) { $forms['phrase4'] = $forms['phrase2']; } if ( !array_key_exists('phrase5', $forms) ) { $forms['phrase5'] = $forms['phrase2']; } $phrase_type = $this->getPluralPhraseType($number); return $this->Application->Phrase($forms['phrase' . $phrase_type], $allow_editing, $use_admin); } /** * Returns phrase type based on given number * * @param int $number * @return int * @access protected */ protected function getPluralPhraseType($number) { $last_digit = substr($number, -1); $last_but_one_digit = strlen($number) > 1 ? substr($number, -2, 1) : false; $phrase_type = 5; if ( $last_but_one_digit != 1 ) { if ( $last_digit >= 1 && $last_digit <= 4 ) { $phrase_type = $last_digit; } } return (string)$phrase_type; } /** * Allows usage of * * @param kEvent $event * @return void * @access public */ public function replaceMLCalculatedFields(kEvent $event) { $config = $event->getUnitConfig(); $editing_language = $this->getEditingLanguage(); $calculated_fields = $config->getSetting('CalculatedFields', Array ()); /* @var $calculated_fields Array */ foreach ($calculated_fields as $special => $fields) { foreach ($fields as $field_name => $field_expression) { $calculated_fields[$special][$field_name] = str_replace('%5$s', $editing_language, $field_expression); } } $config->setSetting('CalculatedFields', $calculated_fields); } /** * Returns language, that is being edited or current language * * @return int * @access public */ public function getEditingLanguage() { $language_id = $this->Application->GetVar('lang_id'); if ( !$language_id ) { $language_id = $this->Application->GetVar('m_lang'); } return $language_id; } /** * Determines if we're editing phrase/e-mail event on it's source language * * @param int $source_language * @return bool * @access public */ public function editingInSourceLanguage($source_language) { return $this->getSourceLanguage($source_language) == $this->getEditingLanguage(); } /** * Replaces source language in given label translation * * @param kDBItem $object * @param string $label * @return string * @access public */ public function replaceSourceLanguage(kDBItem $object, $label) { $ret = $this->Application->Phrase($label); $options = $object->GetFieldOption('TranslateFromLanguage', 'options'); $source_language = $this->getSourceLanguage($object->GetDBField('TranslateFromLanguage')); return sprintf($ret, $options[$source_language]); } /** * Ensures, that primary language is used, when no translation is needed * * @param int $source_language * @return bool * @access public */ public function getSourceLanguage($source_language) { if ( !$source_language ) { $source_language = $this->Application->GetDefaultLanguageId(); } return $source_language; } /** * Translation synchronization state management * * @param kEvent $event * @return void * @access public * @throws InvalidArgumentException */ public function updateTranslationState(kEvent $event) { if ( $event->Name != 'OnBeforeCopyToLive' ) { throw new InvalidArgumentException('Unsupported "' . (string)$event . '" event'); } $object = $event->getObject(Array ('skip_autoload' => true)); /* @var $object kDBItem */ $object->SwitchToTemp(); $object->Load($event->getEventParam('id')); $save_mode = $this->Application->GetVar('translation_save_mode'); if ( $save_mode === false ) { return; } $editing_language = $this->getEditingLanguage(); if ( $save_mode == TranslationSaveMode::SYNC_WITH_PRIMARY ) { $object->SetDBField('l' . $editing_language . '_TranslateFrom', 0); } else { $languages = $this->getLanguages(); foreach ($languages as $language_id) { $object->SetDBField('l' . $language_id . '_TranslateFrom', $language_id == $editing_language ? 0 : $editing_language); } } if ( $object->GetChangedFields() ) { $object->Update(); } } }